diff options
11 files changed, 251 insertions, 352 deletions
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index ccbcba571..ecd1aed69 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ComposerOptions from '../../composer/options'; import ComposerPublisher from '../../composer/publisher'; import ComposerTextarea from '../../composer/textarea'; -import ComposerUploadForm from '../../composer/upload_form'; +import UploadFormContainer from '../containers/upload_form_container'; import PollFormContainer from '../containers/poll_form_container'; import WarningContainer from '../containers/warning_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; @@ -48,7 +48,6 @@ class ComposeForm extends ImmutablePureComponent { media: ImmutablePropTypes.list, preselectDate: PropTypes.instanceOf(Date), privacy: PropTypes.string, - progress: PropTypes.number, resetFileKey: PropTypes.number, sideArm: PropTypes.string, sensitive: PropTypes.bool, @@ -65,7 +64,6 @@ class ComposeForm extends ImmutablePureComponent { // Dispatch props. onChangeAdvancedOption: PropTypes.func, - onChangeDescription: PropTypes.func, onChangeSensitivity: PropTypes.func, onChangeSpoilerText: PropTypes.func, onChangeSpoilerness: PropTypes.func, @@ -80,7 +78,6 @@ class ComposeForm extends ImmutablePureComponent { onOpenDoodleModal: PropTypes.func, onSelectSuggestion: PropTypes.func, onSubmit: PropTypes.func, - onUndoUpload: PropTypes.func, onUnmount: PropTypes.func, onUpload: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, @@ -185,11 +182,6 @@ class ComposeForm extends ImmutablePureComponent { } } - // Sets a reference to the upload form. - handleRefUploadForm = (uploadFormComponent) => { - this.uploadForm = uploadFormComponent; - } - // Sets a reference to the textarea. handleRefTextarea = (textareaComponent) => { if (textareaComponent) { @@ -283,7 +275,6 @@ class ComposeForm extends ImmutablePureComponent { handleSecondarySubmit, handleSelect, handleSubmit, - handleRefUploadForm, handleRefTextarea, } = this; const { @@ -299,7 +290,6 @@ class ComposeForm extends ImmutablePureComponent { media, poll, onChangeAdvancedOption, - onChangeDescription, onChangeSensitivity, onChangeSpoilerness, onChangeText, @@ -310,11 +300,8 @@ class ComposeForm extends ImmutablePureComponent { onFetchSuggestions, onOpenActionsModal, onOpenDoodleModal, - onOpenFocalPointModal, - onUndoUpload, onUpload, privacy, - progress, resetFileKey, sensitive, showSearch, @@ -370,18 +357,7 @@ class ComposeForm extends ImmutablePureComponent { value={text} /> <div className='compose-form__modifiers'> - {isUploading || media && media.size ? ( - <ComposerUploadForm - intl={intl} - media={media} - onChangeDescription={onChangeDescription} - onOpenFocalPointModal={onOpenFocalPointModal} - onRemove={onUndoUpload} - progress={progress} - uploading={isUploading} - handleRef={handleRefUploadForm} - /> - ) : null} + <UploadFormContainer /> <PollFormContainer /> </div> <ComposerOptions diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js new file mode 100644 index 000000000..84edf664e --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload.js @@ -0,0 +1,131 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Motion from 'flavours/glitch/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; +import { isUserTouching } from 'flavours/glitch/util/is_mobile'; + +const messages = defineMessages({ + description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, +}); + +// The component. +export default @injectIntl +class Upload extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + 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, + }; + + 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.context.router.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); + } + } + + render () { + const { intl, media } = this.props; + const active = this.state.hovered || this.state.focused || isUserTouching(); + const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; + const computedClass = classNames('composer--upload_form--item', { active }); + 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; + + return ( + <div className={computedClass} tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'> + <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12, }) }}> + {({ scale }) => ( + <div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> + <div className={classNames('composer--upload_form--actions', { active })}> + <button className='icon-button' onClick={this.handleUndoClick}><Icon icon='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> + {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} + </div> + + <div className={classNames('composer--upload_form--description', { active })}> + <label> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> + <textarea + placeholder={intl.formatMessage(messages.description)} + value={description} + maxLength={420} + onFocus={this.handleInputFocus} + onChange={this.handleInputChange} + onBlur={this.handleInputBlur} + onKeyDown={this.handleKeyDown} + /> + </label> + </div> + </div> + )} + </Motion> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js new file mode 100644 index 000000000..a126cc7e4 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js @@ -0,0 +1,28 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import UploadProgressContainer from '../containers/upload_progress_container'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import UploadContainer from '../containers/upload_container'; + +export default class UploadForm extends ImmutablePureComponent { + static propTypes = { + mediaIds: ImmutablePropTypes.list.isRequired, + }; + + render () { + const { mediaIds } = this.props; + + return ( + <div className='composer--upload_form'> + <UploadProgressContainer /> + + <div className='content'> + {mediaIds.map(id => ( + <UploadContainer id={id} key={id} /> + ))} + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js new file mode 100644 index 000000000..264c563f2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Motion from 'flavours/glitch/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import { FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; + +export default class UploadProgress extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + progress: PropTypes.number, + }; + + render () { + const { active, progress } = this.props; + + if (!active) { + return null; + } + + return ( + <div className='composer--upload_form--progress'> + <Icon icon='upload' /> + + <div className='message'> + <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> + + <div className='backdrop'> + <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> + {({ width }) => + (<div className='tracker' style={{ width: `${width}%` }} + />) + } + </Motion> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index 3293cc226..4716d9435 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -7,14 +7,12 @@ import { changeComposeSpoilerText, changeComposeSpoilerness, changeComposeVisibility, - changeUploadCompose, clearComposeSuggestions, fetchComposeSuggestions, insertEmojiCompose, mountCompose, selectComposeSuggestion, submitCompose, - undoUploadCompose, unmountCompose, uploadCompose, } from 'flavours/glitch/actions/compose'; @@ -66,7 +64,6 @@ function mapStateToProps (state) { media: state.getIn(['compose', 'media_attachments']), preselectDate: state.getIn(['compose', 'preselectDate']), privacy: state.getIn(['compose', 'privacy']), - progress: state.getIn(['compose', 'progress']), resetFileKey: state.getIn(['compose', 'resetFileKey']), sideArm: sideArmPrivacy, sensitive: state.getIn(['compose', 'sensitive']), @@ -89,9 +86,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onChangeAdvancedOption(option, value) { dispatch(changeComposeAdvancedOption(option, value)); }, - onChangeDescription(id, description) { - dispatch(changeUploadCompose(id, { description })); - }, onChangeSensitivity() { dispatch(changeComposeSensitivity()); }, @@ -137,9 +131,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onOpenDoodleModal() { dispatch(openModal('DOODLE', { noEsc: true })); }, - onOpenFocalPointModal(id) { - dispatch(openModal('FOCAL_POINT', { id })); - }, onSelectSuggestion(position, token, suggestion) { dispatch(selectComposeSuggestion(position, token, suggestion)); }, @@ -154,9 +145,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onSubmit(routerHistory) { dispatch(submitCompose(routerHistory)); }, - onUndoUpload(id) { - dispatch(undoUploadCompose(id)); - }, onUnmount() { dispatch(unmountCompose()); }, diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js new file mode 100644 index 000000000..d6bff63ac --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import Upload from '../components/upload'; +import { undoUploadCompose, changeUploadCompose } from 'flavours/glitch/actions/compose'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { submitCompose } from 'flavours/glitch/actions/compose'; + +const mapStateToProps = (state, { id }) => ({ + media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), +}); + +const mapDispatchToProps = dispatch => ({ + + onUndo: id => { + dispatch(undoUploadCompose(id)); + }, + + onDescriptionChange: (id, description) => { + dispatch(changeUploadCompose(id, { description })); + }, + + onOpenFocalPoint: id => { + dispatch(openModal('FOCAL_POINT', { id })); + }, + + onSubmit (router) { + dispatch(submitCompose(router)); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Upload); diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js new file mode 100644 index 000000000..a6798bf51 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import UploadForm from '../components/upload_form'; + +const mapStateToProps = state => ({ + mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')), +}); + +export default connect(mapStateToProps)(UploadForm); diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js new file mode 100644 index 000000000..0cfee96da --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux'; +import UploadProgress from '../components/upload_progress'; + +const mapStateToProps = state => ({ + active: state.getIn(['compose', 'is_uploading']), + progress: state.getIn(['compose', 'progress']), +}); + +export default connect(mapStateToProps)(UploadProgress); diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js deleted file mode 100644 index c2ff66623..000000000 --- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js +++ /dev/null @@ -1,60 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Components. -import ComposerUploadFormItem from './item'; -import ComposerUploadFormProgress from './progress'; - -// The component. -export default function ComposerUploadForm ({ - intl, - media, - onChangeDescription, - onOpenFocalPointModal, - onRemove, - progress, - uploading, - handleRef, -}) { - const computedClass = classNames('composer--upload_form', { uploading }); - - // The result. - return ( - <div className={computedClass} ref={handleRef}> - {uploading ? <ComposerUploadFormProgress progress={progress} /> : null} - {media ? ( - <div className='content'> - {media.map(item => ( - <ComposerUploadFormItem - description={item.get('description')} - key={item.get('id')} - id={item.get('id')} - intl={intl} - focusX={item.getIn(['meta', 'focus', 'x'])} - focusY={item.getIn(['meta', 'focus', 'y'])} - mediaType={item.get('type')} - preview={item.get('preview_url')} - onChangeDescription={onChangeDescription} - onOpenFocalPointModal={onOpenFocalPointModal} - onRemove={onRemove} - /> - ))} - </div> - ) : null} - </div> - ); -} - -// Props. -ComposerUploadForm.propTypes = { - intl: PropTypes.object.isRequired, - media: ImmutablePropTypes.list, - onChangeDescription: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - progress: PropTypes.number, - uploading: PropTypes.bool, - handleRef: PropTypes.func, -}; diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js deleted file mode 100644 index 4f5f66f04..000000000 --- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js +++ /dev/null @@ -1,202 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { - FormattedMessage, - defineMessages, -} from 'react-intl'; -import spring from 'react-motion/lib/spring'; - -// Components. -import IconButton from 'flavours/glitch/components/icon_button'; - -// Utils. -import Motion from 'flavours/glitch/util/optional_motion'; -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -import { isUserTouching } from 'flavours/glitch/util/is_mobile'; - -// Messages. -const messages = defineMessages({ - undo: { - defaultMessage: 'Undo', - id: 'upload_form.undo', - }, - description: { - defaultMessage: 'Describe for the visually impaired', - id: 'upload_form.description', - }, - crop: { - defaultMessage: 'Crop', - id: 'upload_form.focus', - }, -}); - -// Handlers. -const handlers = { - - // On blur, we save the description for the media item. - handleBlur () { - const { - id, - onChangeDescription, - } = this.props; - const { dirtyDescription } = this.state; - - this.setState({ dirtyDescription: null, focused: false }); - - if (id && onChangeDescription && dirtyDescription !== null) { - onChangeDescription(id, dirtyDescription); - } - }, - - // When the value of our description changes, we store it in the - // temp value `dirtyDescription` in our state. - handleChange ({ target: { value } }) { - this.setState({ dirtyDescription: value }); - }, - - // Records focus on the media item. - handleFocus () { - this.setState({ focused: true }); - }, - - // Records the start of a hover over the media item. - handleMouseEnter () { - this.setState({ hovered: true }); - }, - - // Records the end of a hover over the media item. - handleMouseLeave () { - this.setState({ hovered: false }); - }, - - // Removes the media item. - handleRemove () { - const { - id, - onRemove, - } = this.props; - if (id && onRemove) { - onRemove(id); - } - }, - - // Opens the focal point modal. - handleFocalPointClick () { - const { - id, - onOpenFocalPointModal, - } = this.props; - if (id && onOpenFocalPointModal) { - onOpenFocalPointModal(id); - } - }, -}; - -// The component. -export default class ComposerUploadFormItem extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - this.state = { - hovered: false, - focused: false, - dirtyDescription: null, - }; - } - - // Rendering. - render () { - const { - handleBlur, - handleChange, - handleFocus, - handleMouseEnter, - handleMouseLeave, - handleRemove, - handleFocalPointClick, - } = this.handlers; - const { - intl, - preview, - focusX, - focusY, - mediaType, - } = this.props; - const { - focused, - hovered, - dirtyDescription, - } = this.state; - const active = hovered || focused || isUserTouching(); - const computedClass = classNames('composer--upload_form--item', { active }); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const description = dirtyDescription || (dirtyDescription !== '' && this.props.description) || ''; - - // The result. - return ( - <div - className={computedClass} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - > - <Motion - defaultStyle={{ scale: 0.8 }} - style={{ - scale: spring(1, { - stiffness: 180, - damping: 12, - }), - }} - > - {({ scale }) => ( - <div - style={{ - transform: `scale(${scale})`, - backgroundImage: preview ? `url(${preview})` : null, - backgroundPosition: `${x}% ${y}%` - }} - > - <div className={classNames('composer--upload_form--actions', { active })}> - <button className='icon-button' onClick={handleRemove}> - <i className='fa fa-times' /> <FormattedMessage {...messages.undo} /> - </button> - {mediaType === 'image' && <button className='icon-button' onClick={handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage {...messages.crop} /></button>} - </div> - <label> - <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span> - <textarea - maxLength={420} - onBlur={handleBlur} - onChange={handleChange} - onFocus={handleFocus} - placeholder={intl.formatMessage(messages.description)} - value={description} - /> - </label> - </div> - )} - </Motion> - </div> - ); - } - -} - -// Props. -ComposerUploadFormItem.propTypes = { - description: PropTypes.string, - id: PropTypes.string, - intl: PropTypes.object.isRequired, - onChangeDescription: PropTypes.func.isRequired, - onOpenFocalPointModal: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - focusX: PropTypes.number, - focusY: PropTypes.number, - mediaType: PropTypes.string, - preview: PropTypes.string, -}; diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js deleted file mode 100644 index 8c4b0eea6..000000000 --- a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js +++ /dev/null @@ -1,52 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import { - defineMessages, - FormattedMessage, -} from 'react-intl'; -import spring from 'react-motion/lib/spring'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import Motion from 'flavours/glitch/util/optional_motion'; - -// Messages. -const messages = defineMessages({ - upload: { - defaultMessage: 'Uploading...', - id: 'upload_progress.label', - }, -}); - -// The component. -export default function ComposerUploadFormProgress ({ progress }) { - - // The result. - return ( - <div className='composer--upload_form--progress'> - <Icon icon='upload' /> - <div className='message'> - <FormattedMessage {...messages.upload} /> - <div className='backdrop'> - <Motion - defaultStyle={{ width: 0 }} - style={{ width: spring(progress) }} - > - {({ width }) => - (<div - className='tracker' - style={{ width: `${width}%` }} - />) - } - </Motion> - </div> - </div> - </div> - ); -} - -// Props. -ComposerUploadFormProgress.propTypes = { progress: PropTypes.number }; |