diff options
13 files changed, 244 insertions, 75 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 165e811e3..51c11e5ad 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 38deeae0e..744424661 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 729d00617..44added8a 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 ( <div style={{ position: 'relative' }}> - <div style={{...iconStyle }} className={collapsedClassName} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div> + <div style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div> <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}> {({ 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 047c974f2..2a252af4b 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 3a454a5fb..37e366203 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 ( <Dropdown ref={this.setRef} style={{ marginLeft: '5px' }}> - <DropdownTrigger className='icon-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}> - <i className={`fa fa-smile-o`} style={{ verticalAlign: 'middle' }} /> + <DropdownTrigger className='icon-button emoji-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}> + <img className="emojione" alt="🙂" src="/emoji/1f642.png" /> </DropdownTrigger> <DropdownContent> 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 94c94b4b7..1a01c2380 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 => ( - <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'> - <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}> - <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> - </div> + const uploads = media.map(attachment => + <div key={attachment.get('id')} style={{ marginBottom: '10px' }}> + <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> + {({ scale }) => + <div style={{ transform: `translateZ(0) scale(${scale})`, width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}> + <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> + </div> + } + </Motion> </div> - )); + ); return ( <div style={{ marginBottom: '20px', padding: '10px', overflow: 'hidden', flexShrink: '0' }}> + <UploadProgressContainer /> {uploads} </div> ); 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 000000000..86ffbf936 --- /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 ( + <div className='upload-progress'> + <div> + <i className='fa fa-upload' /> + </div> + + <div style={{ flex: '1 1 auto' }}> + <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> + + <div className='upload-progress__backdrop'> + <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> + {({ width }) => + <div className='upload-progress__tracker' style={{ width: `${width}%` }} /> + } + </Motion> + </div> + </div> + </div> + ); + } + +}); + +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 a67adbdd6..835b37516 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 000000000..b0f1d4d19 --- /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 000000000..70b687019 --- /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 ( + <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}> + {({ backgroundOpacity, backgroundScale }) => + <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> + <div className='upload-area__drop'> + <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} /> + <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div> + </div> + </div> + } + </Motion> + ); + } + +}); + +export default UploadArea; diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 900d83dba..10e989b2a 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 = ( <ColumnsArea> - {this.props.children} + {children} </ColumnsArea> ); } else { @@ -83,13 +93,13 @@ const UI = React.createClass({ <Compose withHeader={true} /> <HomeTimeline trackScroll={false} /> <Notifications trackScroll={false} /> - {this.props.children} + {children} </ColumnsArea> ); } return ( - <div className='ui'> + <div className='ui' onDragLeave={this.handleDragLeave}> <TabsBar /> {mountedColumns} @@ -97,6 +107,7 @@ const UI = React.createClass({ <NotificationsContainer /> <LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} /> <ModalContainer /> + <UploadArea active={draggingOver} /> </div> ); } diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index b0001351f..a4bdd20cd 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 057c61f91..bda35425f 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; +} |