From b899b3def20cab23d92a66f04674635b67c175ff Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 24 Mar 2017 03:50:30 +0100 Subject: [PATCH] Fancier drag & drop indicator, emoji icon for emoji, upload progress (fix #295) --- .../components/actions/compose.jsx | 9 +- .../components/autosuggest_textarea.jsx | 40 +----- .../components/column_collapsable.jsx | 2 +- .../compose/components/compose_form.jsx | 2 - .../components/emoji_picker_dropdown.jsx | 6 +- .../compose/components/upload_form.jsx | 24 ++-- .../compose/components/upload_progress.jsx | 44 ++++++ .../containers/compose_form_container.jsx | 1 - .../containers/upload_progress_container.jsx | 9 ++ .../features/ui/components/upload_area.jsx | 32 +++++ .../components/features/ui/index.jsx | 23 +++- .../components/reducers/compose.jsx | 2 - app/assets/stylesheets/components.scss | 125 ++++++++++++++++-- 13 files changed, 244 insertions(+), 75 deletions(-) create mode 100644 app/assets/javascripts/components/features/compose/components/upload_progress.jsx create mode 100644 app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/upload_area.jsx diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 165e811e38..51c11e5ad9 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -142,7 +142,8 @@ export function uploadCompose(files) { export function uploadComposeRequest() { return { - type: COMPOSE_UPLOAD_REQUEST + type: COMPOSE_UPLOAD_REQUEST, + skipLoading: true }; }; @@ -157,14 +158,16 @@ export function uploadComposeProgress(loaded, total) { export function uploadComposeSuccess(media) { return { type: COMPOSE_UPLOAD_SUCCESS, - media: media + media: media, + skipLoading: true }; }; export function uploadComposeFail(error) { return { type: COMPOSE_UPLOAD_FAIL, - error: error + error: error, + skipLoading: true }; }; diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx index 38deeae0e3..7444246612 100644 --- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx +++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx @@ -33,7 +33,6 @@ const AutosuggestTextarea = React.createClass({ value: React.PropTypes.string, suggestions: ImmutablePropTypes.list, disabled: React.PropTypes.bool, - fileDropDate: React.PropTypes.instanceOf(Date), placeholder: React.PropTypes.string, onSuggestionSelected: React.PropTypes.func.isRequired, onSuggestionsClearRequested: React.PropTypes.func.isRequired, @@ -46,8 +45,6 @@ const AutosuggestTextarea = React.createClass({ getInitialState () { return { - isFileDragging: false, - fileDraggingDate: undefined, suggestionsHidden: false, selectedSuggestion: 0, lastToken: null, @@ -139,41 +136,12 @@ const AutosuggestTextarea = React.createClass({ if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { this.setState({ suggestionsHidden: false }); } - - const fileDropDate = nextProps.fileDropDate; - const { isFileDragging, fileDraggingDate } = this.state; - - /* - * We can't detect drop events, because they might not be on the textarea (the app allows dropping anywhere in the - * window). Instead, on-drop, we notify this textarea to stop its hover effect by passing in a prop with the - * drop-date. - */ - if (isFileDragging && fileDraggingDate && fileDropDate // if dragging when props updated, and dates aren't undefined - && fileDropDate > fileDraggingDate) { // and if the drop date is now greater than when we started dragging - // then we should stop dragging - this.setState({ - isFileDragging: false - }); - } }, setTextarea (c) { this.textarea = c; }, - onDragEnter () { - this.setState({ - isFileDragging: true, - fileDraggingDate: new Date() - }) - }, - - onDragExit () { - this.setState({ - isFileDragging: false - }) - }, - onPaste (e) { if (e.clipboardData && e.clipboardData.files.length === 1) { this.props.onPaste(e.clipboardData.files) @@ -182,9 +150,9 @@ const AutosuggestTextarea = React.createClass({ }, render () { - const { value, suggestions, fileDropDate, disabled, placeholder, onKeyUp } = this.props; - const { isFileDragging, suggestionsHidden, selectedSuggestion } = this.state; - const className = isFileDragging ? 'autosuggest-textarea__textarea file-drop' : 'autosuggest-textarea__textarea'; + const { value, suggestions, disabled, placeholder, onKeyUp } = this.props; + const { suggestionsHidden, selectedSuggestion } = this.state; + const className = 'autosuggest-textarea__textarea'; const style = { direction: 'ltr' }; if (isRtl(value)) { @@ -204,8 +172,6 @@ const AutosuggestTextarea = React.createClass({ onKeyDown={this.onKeyDown} onKeyUp={onKeyUp} onBlur={this.onBlur} - onDragEnter={this.onDragEnter} - onDragExit={this.onDragExit} onPaste={this.onPaste} style={style} /> diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx index 729d006174..44added8a0 100644 --- a/app/assets/javascripts/components/components/column_collapsable.jsx +++ b/app/assets/javascripts/components/components/column_collapsable.jsx @@ -45,7 +45,7 @@ const ColumnCollapsable = React.createClass({ return (
-
+
{({ opacity, height }) => diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index 047c974f29..2a252af4b2 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -34,7 +34,6 @@ const ComposeForm = React.createClass({ private: React.PropTypes.bool, unlisted: React.PropTypes.bool, spoiler_text: React.PropTypes.string, - fileDropDate: React.PropTypes.instanceOf(Date), focusDate: React.PropTypes.instanceOf(Date), preselectDate: React.PropTypes.instanceOf(Date), is_submitting: React.PropTypes.bool, @@ -161,7 +160,6 @@ const ComposeForm = React.createClass({ ref={this.setAutosuggestTextarea} placeholder={intl.formatMessage(messages.placeholder)} disabled={disabled} - fileDropDate={this.props.fileDropDate} value={this.props.text} onChange={this.handleChange} suggestions={this.props.suggestions} diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx index 3a454a5fbd..37e366203c 100644 --- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -4,7 +4,7 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ - emoji: { id: 'emoji_button.label', defaultMessage: 'Emoji' } + emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' } }); const settings = { @@ -36,8 +36,8 @@ const EmojiPickerDropdown = React.createClass({ return ( - - + + 🙂 diff --git a/app/assets/javascripts/components/features/compose/components/upload_form.jsx b/app/assets/javascripts/components/features/compose/components/upload_form.jsx index 94c94b4b74..1a01c2380b 100644 --- a/app/assets/javascripts/components/features/compose/components/upload_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/upload_form.jsx @@ -2,6 +2,8 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from '../../../components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; +import UploadProgressContainer from '../containers/upload_progress_container'; +import { Motion, spring } from 'react-motion'; const messages = defineMessages({ undo: { id: 'upload_form.undo', defaultMessage: 'Undo' } @@ -11,7 +13,6 @@ const UploadForm = React.createClass({ propTypes: { media: ImmutablePropTypes.list.isRequired, - is_uploading: React.PropTypes.bool, onRemoveFile: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired }, @@ -21,20 +22,21 @@ const UploadForm = React.createClass({ render () { const { intl, media } = this.props; - if (!media.size) { - return null; - } - - const uploads = media.map(attachment => ( -
-
- -
+ const uploads = media.map(attachment => +
+ + {({ scale }) => +
+ +
+ } +
- )); + ); return (
+ {uploads}
); diff --git a/app/assets/javascripts/components/features/compose/components/upload_progress.jsx b/app/assets/javascripts/components/features/compose/components/upload_progress.jsx new file mode 100644 index 0000000000..86ffbf9367 --- /dev/null +++ b/app/assets/javascripts/components/features/compose/components/upload_progress.jsx @@ -0,0 +1,44 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import { Motion, spring } from 'react-motion'; +import { FormattedMessage } from 'react-intl'; + +const UploadProgress = React.createClass({ + + propTypes: { + active: React.PropTypes.bool, + progress: React.PropTypes.number + }, + + mixins: [PureRenderMixin], + + render () { + const { active, progress } = this.props; + + if (!active) { + return null; + } + + return ( +
+
+ +
+ +
+ + +
+ + {({ width }) => +
+ } + +
+
+
+ ); + } + +}); + +export default UploadProgress; diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx index a67adbdd62..835b375166 100644 --- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx @@ -30,7 +30,6 @@ const mapStateToProps = (state, props) => { spoiler_text: state.getIn(['compose', 'spoiler_text']), unlisted: state.getIn(['compose', 'unlisted'], ), private: state.getIn(['compose', 'private']), - fileDropDate: state.getIn(['compose', 'fileDropDate']), focusDate: state.getIn(['compose', 'focusDate']), preselectDate: state.getIn(['compose', 'preselectDate']), is_submitting: state.getIn(['compose', 'is_submitting']), diff --git a/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx b/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx new file mode 100644 index 0000000000..b0f1d4d193 --- /dev/null +++ b/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx @@ -0,0 +1,9 @@ +import { connect } from 'react-redux'; +import UploadProgress from '../components/upload_progress'; + +const mapStateToProps = (state, props) => ({ + active: state.getIn(['compose', 'is_uploading']), + progress: state.getIn(['compose', 'progress']) +}); + +export default connect(mapStateToProps)(UploadProgress); diff --git a/app/assets/javascripts/components/features/ui/components/upload_area.jsx b/app/assets/javascripts/components/features/ui/components/upload_area.jsx new file mode 100644 index 0000000000..70b6870191 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/upload_area.jsx @@ -0,0 +1,32 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import { Motion, spring } from 'react-motion'; +import { FormattedMessage } from 'react-intl'; + +const UploadArea = React.createClass({ + + propTypes: { + active: React.PropTypes.bool + }, + + mixins: [PureRenderMixin], + + render () { + const { active } = this.props; + + return ( + + {({ backgroundOpacity, backgroundScale }) => +
+
+
+
+
+
+ } + + ); + } + +}); + +export default UploadArea; diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 900d83dba0..10e989b2a4 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -13,6 +13,7 @@ import { debounce } from 'react-decoration'; import { uploadCompose } from '../../actions/compose'; import { refreshTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; +import UploadArea from './components/upload_area'; const UI = React.createClass({ @@ -23,7 +24,8 @@ const UI = React.createClass({ getInitialState () { return { - width: window.innerWidth + width: window.innerWidth, + draggingOver: false }; }, @@ -41,7 +43,7 @@ const UI = React.createClass({ e.dataTransfer.dropEffect = 'copy'; if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') { - // + this.setState({ draggingOver: true }); } }, @@ -49,10 +51,15 @@ const UI = React.createClass({ e.preventDefault(); if (e.dataTransfer && e.dataTransfer.files.length === 1) { + this.setState({ draggingOver: false }); this.props.dispatch(uploadCompose(e.dataTransfer.files)); } }, + handleDragLeave () { + this.setState({ draggingOver: false }); + }, + componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); window.addEventListener('dragover', this.handleDragOver); @@ -69,12 +76,15 @@ const UI = React.createClass({ }, render () { + const { width, draggingOver } = this.state; + const { children } = this.props; + let mountedColumns; - if (isMobile(this.state.width)) { + if (isMobile(width)) { mountedColumns = ( - {this.props.children} + {children} ); } else { @@ -83,13 +93,13 @@ const UI = React.createClass({ - {this.props.children} + {children} ); } return ( -
+
{mountedColumns} @@ -97,6 +107,7 @@ const UI = React.createClass({ +
); } diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index b0001351f9..a4bdd20cd7 100644 --- a/app/assets/javascripts/components/reducers/compose.jsx +++ b/app/assets/javascripts/components/reducers/compose.jsx @@ -35,7 +35,6 @@ const initialState = Immutable.Map({ unlisted: false, private: false, text: '', - fileDropDate: null, focusDate: null, preselectDate: null, in_reply_to: null, @@ -163,7 +162,6 @@ export default function compose(state = initialState, action) { case COMPOSE_UPLOAD_REQUEST: return state.withMutations(map => { map.set('is_uploading', true); - map.set('fileDropDate', new Date()); }); case COMPOSE_UPLOAD_SUCCESS: return appendMedia(state, Immutable.fromJS(action.media)); diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 057c61f911..bda35425fa 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -960,17 +960,11 @@ a.status__content__spoiler-link { resize: none; margin: 0; color: $color1; - padding: 7px; + padding: 10px; font-family: inherit; font-size: 14px; resize: vertical; - - border: 3px dashed transparent; - transition: border-color 0.3s ease; - - &.file-drop { - border-color: darken($color5, 33%); - } + border: 0; } .autosuggest-textarea__textarea { @@ -1094,7 +1088,7 @@ button.active i.fa-retweet { text-decoration: none; &:hover { - background: lighten($color1, 8%); + background: lighten($color1, 2%); } } @@ -1431,3 +1425,116 @@ button.active i.fa-retweet { font-weight: 500; } } + +.upload-area { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + visibility: hidden; + background: rgba($color8, 0.8); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + z-index: 2000; + + * { + pointer-events: none; + } +} + +.upload-area__drop { + width: 320px; + height: 160px; + display: flex; + box-sizing: border-box; + position: relative; + padding: 8px; +} + +.upload-area__background { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + border-radius: 4px; + background: $color1; + box-shadow: 0 0 5px rgba($color8, 0.2); +} + +.upload-area__content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: $color2; + font-size: 18px; + font-weight: 500; + border: 2px dashed lighten($color1, 26%); + border-radius: 4px; +} + +.upload-progress { + padding-bottom: 20px; + color: lighten($color1, 26%); + overflow: hidden; + display: flex; + + .fa { + font-size: 34px; + margin-right: 10px; + } + + span { + font-size: 12px; + text-transform: uppercase; + font-weight: 500; + display: block; + } +} + +.upload-progress__backdrop { + width: 100%; + height: 6px; + border-radius: 6px; + background: lighten($color1, 26%); + position: relative; + margin-top: 5px; +} + +.upload-progress__tracker { + position: absolute; + left: 0; + top: 0; + height: 6px; + background: $color2; + border-radius: 6px; +} + +.emoji-button { + img { + filter: grayscale(100%); + opacity: 0.4; + display: block; + margin: 0; + width: 22px; + height: 22px; + margin-top: 2px; + } + + &:hover { + img { + opacity: 1; + filter: none; + } + } +} + +.dropdown--active .emoji-button img { + opacity: 1; + filter: none; +}