ComposerUploadForm → UploadForm + UploadFormContainer

pull/574/head
Thibaut Girka 2019-04-21 12:09:52 +02:00 committed by ThibG
parent c5f49a92dc
commit a243567a3e
11 changed files with 251 additions and 352 deletions

View File

@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ComposerOptions from '../../composer/options'; import ComposerOptions from '../../composer/options';
import ComposerPublisher from '../../composer/publisher'; import ComposerPublisher from '../../composer/publisher';
import ComposerTextarea from '../../composer/textarea'; 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 PollFormContainer from '../containers/poll_form_container';
import WarningContainer from '../containers/warning_container'; import WarningContainer from '../containers/warning_container';
import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container';
@ -48,7 +48,6 @@ class ComposeForm extends ImmutablePureComponent {
media: ImmutablePropTypes.list, media: ImmutablePropTypes.list,
preselectDate: PropTypes.instanceOf(Date), preselectDate: PropTypes.instanceOf(Date),
privacy: PropTypes.string, privacy: PropTypes.string,
progress: PropTypes.number,
resetFileKey: PropTypes.number, resetFileKey: PropTypes.number,
sideArm: PropTypes.string, sideArm: PropTypes.string,
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
@ -65,7 +64,6 @@ class ComposeForm extends ImmutablePureComponent {
// Dispatch props. // Dispatch props.
onChangeAdvancedOption: PropTypes.func, onChangeAdvancedOption: PropTypes.func,
onChangeDescription: PropTypes.func,
onChangeSensitivity: PropTypes.func, onChangeSensitivity: PropTypes.func,
onChangeSpoilerText: PropTypes.func, onChangeSpoilerText: PropTypes.func,
onChangeSpoilerness: PropTypes.func, onChangeSpoilerness: PropTypes.func,
@ -80,7 +78,6 @@ class ComposeForm extends ImmutablePureComponent {
onOpenDoodleModal: PropTypes.func, onOpenDoodleModal: PropTypes.func,
onSelectSuggestion: PropTypes.func, onSelectSuggestion: PropTypes.func,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
onUndoUpload: PropTypes.func,
onUnmount: PropTypes.func, onUnmount: PropTypes.func,
onUpload: PropTypes.func, onUpload: PropTypes.func,
onMediaDescriptionConfirm: 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. // Sets a reference to the textarea.
handleRefTextarea = (textareaComponent) => { handleRefTextarea = (textareaComponent) => {
if (textareaComponent) { if (textareaComponent) {
@ -283,7 +275,6 @@ class ComposeForm extends ImmutablePureComponent {
handleSecondarySubmit, handleSecondarySubmit,
handleSelect, handleSelect,
handleSubmit, handleSubmit,
handleRefUploadForm,
handleRefTextarea, handleRefTextarea,
} = this; } = this;
const { const {
@ -299,7 +290,6 @@ class ComposeForm extends ImmutablePureComponent {
media, media,
poll, poll,
onChangeAdvancedOption, onChangeAdvancedOption,
onChangeDescription,
onChangeSensitivity, onChangeSensitivity,
onChangeSpoilerness, onChangeSpoilerness,
onChangeText, onChangeText,
@ -310,11 +300,8 @@ class ComposeForm extends ImmutablePureComponent {
onFetchSuggestions, onFetchSuggestions,
onOpenActionsModal, onOpenActionsModal,
onOpenDoodleModal, onOpenDoodleModal,
onOpenFocalPointModal,
onUndoUpload,
onUpload, onUpload,
privacy, privacy,
progress,
resetFileKey, resetFileKey,
sensitive, sensitive,
showSearch, showSearch,
@ -370,18 +357,7 @@ class ComposeForm extends ImmutablePureComponent {
value={text} value={text}
/> />
<div className='compose-form__modifiers'> <div className='compose-form__modifiers'>
{isUploading || media && media.size ? ( <UploadFormContainer />
<ComposerUploadForm
intl={intl}
media={media}
onChangeDescription={onChangeDescription}
onOpenFocalPointModal={onOpenFocalPointModal}
onRemove={onUndoUpload}
progress={progress}
uploading={isUploading}
handleRef={handleRefUploadForm}
/>
) : null}
<PollFormContainer /> <PollFormContainer />
</div> </div>
<ComposerOptions <ComposerOptions

View File

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

View File

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

View File

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

View File

@ -7,14 +7,12 @@ import {
changeComposeSpoilerText, changeComposeSpoilerText,
changeComposeSpoilerness, changeComposeSpoilerness,
changeComposeVisibility, changeComposeVisibility,
changeUploadCompose,
clearComposeSuggestions, clearComposeSuggestions,
fetchComposeSuggestions, fetchComposeSuggestions,
insertEmojiCompose, insertEmojiCompose,
mountCompose, mountCompose,
selectComposeSuggestion, selectComposeSuggestion,
submitCompose, submitCompose,
undoUploadCompose,
unmountCompose, unmountCompose,
uploadCompose, uploadCompose,
} from 'flavours/glitch/actions/compose'; } from 'flavours/glitch/actions/compose';
@ -66,7 +64,6 @@ function mapStateToProps (state) {
media: state.getIn(['compose', 'media_attachments']), media: state.getIn(['compose', 'media_attachments']),
preselectDate: state.getIn(['compose', 'preselectDate']), preselectDate: state.getIn(['compose', 'preselectDate']),
privacy: state.getIn(['compose', 'privacy']), privacy: state.getIn(['compose', 'privacy']),
progress: state.getIn(['compose', 'progress']),
resetFileKey: state.getIn(['compose', 'resetFileKey']), resetFileKey: state.getIn(['compose', 'resetFileKey']),
sideArm: sideArmPrivacy, sideArm: sideArmPrivacy,
sensitive: state.getIn(['compose', 'sensitive']), sensitive: state.getIn(['compose', 'sensitive']),
@ -89,9 +86,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onChangeAdvancedOption(option, value) { onChangeAdvancedOption(option, value) {
dispatch(changeComposeAdvancedOption(option, value)); dispatch(changeComposeAdvancedOption(option, value));
}, },
onChangeDescription(id, description) {
dispatch(changeUploadCompose(id, { description }));
},
onChangeSensitivity() { onChangeSensitivity() {
dispatch(changeComposeSensitivity()); dispatch(changeComposeSensitivity());
}, },
@ -137,9 +131,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onOpenDoodleModal() { onOpenDoodleModal() {
dispatch(openModal('DOODLE', { noEsc: true })); dispatch(openModal('DOODLE', { noEsc: true }));
}, },
onOpenFocalPointModal(id) {
dispatch(openModal('FOCAL_POINT', { id }));
},
onSelectSuggestion(position, token, suggestion) { onSelectSuggestion(position, token, suggestion) {
dispatch(selectComposeSuggestion(position, token, suggestion)); dispatch(selectComposeSuggestion(position, token, suggestion));
}, },
@ -154,9 +145,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onSubmit(routerHistory) { onSubmit(routerHistory) {
dispatch(submitCompose(routerHistory)); dispatch(submitCompose(routerHistory));
}, },
onUndoUpload(id) {
dispatch(undoUploadCompose(id));
},
onUnmount() { onUnmount() {
dispatch(unmountCompose()); dispatch(unmountCompose());
}, },

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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 };