diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2020-07-07 12:14:19 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-07 12:14:19 +0200 |
commit | 06fc6a9cd4c700c450dbe8e8c579a5d5bc4ba732 (patch) | |
tree | b70bab5da69ba49da6a72cb9d83622abaa308eae | |
parent | ed04697510d6207772c3f256a155a99754caac51 (diff) |
Add ability to choose media thumbnail in web UI (#14244)
-rw-r--r-- | app/javascript/mastodon/actions/compose.js | 49 | ||||
-rw-r--r-- | app/javascript/mastodon/features/ui/components/focal_point_modal.js | 55 | ||||
-rw-r--r-- | app/javascript/mastodon/reducers/compose.js | 22 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/components.scss | 9 |
4 files changed, 132 insertions, 3 deletions
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 6b73fc90e..20341f9ec 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -28,6 +28,11 @@ export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; +export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST'; +export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS'; +export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL'; +export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS'; + export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; @@ -260,6 +265,49 @@ export function uploadCompose(files) { }; }; +export const uploadThumbnail = (id, file) => (dispatch, getState) => { + dispatch(uploadThumbnailRequest()); + + const total = file.size; + const data = new FormData(); + + data.append('thumbnail', file); + + api(getState).put(`/api/v1/media/${id}`, data, { + onUploadProgress: ({ loaded }) => { + dispatch(uploadThumbnailProgress(loaded, total)); + }, + }).then(({ data }) => { + dispatch(uploadThumbnailSuccess(data)); + }).catch(error => { + dispatch(uploadThumbnailFail(id, error)); + }); +}; + +export const uploadThumbnailRequest = () => ({ + type: THUMBNAIL_UPLOAD_REQUEST, + skipLoading: true, +}); + +export const uploadThumbnailProgress = (loaded, total) => ({ + type: THUMBNAIL_UPLOAD_PROGRESS, + loaded, + total, + skipLoading: true, +}); + +export const uploadThumbnailSuccess = media => ({ + type: THUMBNAIL_UPLOAD_SUCCESS, + media, + skipLoading: true, +}); + +export const uploadThumbnailFail = error => ({ + type: THUMBNAIL_UPLOAD_FAIL, + error, + skipLoading: true, +}); + export function changeUploadCompose(id, params) { return (dispatch, getState) => { dispatch(changeUploadComposeRequest()); @@ -278,6 +326,7 @@ export function changeUploadComposeRequest() { skipLoading: true, }; }; + export function changeUploadComposeSuccess(media) { return { type: COMPOSE_UPLOAD_CHANGE_SUCCESS, diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index 8112e3b9e..7348d9599 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import classNames from 'classnames'; -import { changeUploadCompose } from '../../../actions/compose'; +import { changeUploadCompose, uploadThumbnail } from '../../../actions/compose'; import { getPointerPosition } from '../../video'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import IconButton from 'mastodon/components/icon_button'; @@ -23,11 +23,13 @@ const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' }, placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' }, + chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' }, }); const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), account: state.getIn(['accounts', me]), + isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']), }); const mapDispatchToProps = (dispatch, { id }) => ({ @@ -36,6 +38,10 @@ const mapDispatchToProps = (dispatch, { id }) => ({ dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); }, + onSelectThumbnail: files => { + dispatch(uploadThumbnail(id, files[0])); + }, + }); const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') @@ -81,6 +87,9 @@ class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired, + isUploadingThumbnail: PropTypes.bool, + onSave: PropTypes.func.isRequired, + onSelectThumbnail: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -235,13 +244,29 @@ class FocalPointModal extends ImmutablePureComponent { }).catch(() => this.setState({ detecting: false })); } + handleThumbnailChange = e => { + if (e.target.files.length > 0) { + this.setState({ dirty: true }); + this.props.onSelectThumbnail(e.target.files); + } + } + + setFileInputRef = c => { + this.fileInput = c; + } + + handleFileInputClick = () => { + this.fileInput.click(); + } + render () { - const { media, intl, account, onClose } = this.props; + const { media, intl, account, onClose, isUploadingThumbnail } = this.props; const { x, y, dragging, description, dirty, detecting, progress } = this.state; const width = media.getIn(['meta', 'original', 'width']) || null; const height = media.getIn(['meta', 'original', 'height']) || null; const focals = ['image', 'gifv'].includes(media.get('type')); + const thumbnailable = ['audio', 'video'].includes(media.get('type')); const previewRatio = 16/9; const previewWidth = 200; @@ -268,6 +293,30 @@ class FocalPointModal extends ImmutablePureComponent { <div className='report-modal__comment'> {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>} + {thumbnailable && ( + <React.Fragment> + <label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label> + + <Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} /> + + <label> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span> + + <input + id='upload-modal__thumbnail' + ref={this.setFileInputRef} + type='file' + accept='image/png,image/jpeg' + onChange={this.handleThumbnailChange} + style={{ display: 'none' }} + disabled={isUploadingThumbnail} + /> + </label> + + <hr className='setting-divider' /> + </React.Fragment> + )} + <label className='setting-text-label' htmlFor='upload-modal__description'> {descriptionLabel} </label> @@ -293,7 +342,7 @@ class FocalPointModal extends ImmutablePureComponent { <CharacterCounter max={1500} text={detecting ? '' : description} /> </div> - <Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> + <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> </div> <div className='focal-point-modal__content'> diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index e6e6d2ae1..4c0ba1c36 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -14,6 +14,10 @@ import { COMPOSE_UPLOAD_FAIL, COMPOSE_UPLOAD_UNDO, COMPOSE_UPLOAD_PROGRESS, + THUMBNAIL_UPLOAD_REQUEST, + THUMBNAIL_UPLOAD_SUCCESS, + THUMBNAIL_UPLOAD_FAIL, + THUMBNAIL_UPLOAD_PROGRESS, COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTION_SELECT, @@ -60,6 +64,8 @@ const initialState = ImmutableMap({ is_changing_upload: false, is_uploading: false, progress: 0, + isUploadingThumbnail: false, + thumbnailProgress: 0, media_attachments: ImmutableList(), pending_media_attachments: 0, poll: null, @@ -332,6 +338,22 @@ export default function compose(state = initialState, action) { return removeMedia(state, action.media_id); case COMPOSE_UPLOAD_PROGRESS: return state.set('progress', Math.round((action.loaded / action.total) * 100)); + case THUMBNAIL_UPLOAD_REQUEST: + return state.set('isUploadingThumbnail', true); + case THUMBNAIL_UPLOAD_PROGRESS: + return state.set('thumbnailProgress', Math.round((action.loaded / action.total) * 100)); + case THUMBNAIL_UPLOAD_FAIL: + return state.set('isUploadingThumbnail', false); + case THUMBNAIL_UPLOAD_SUCCESS: + return state + .set('isUploadingThumbnail', false) + .update('media_attachments', list => list.map(item => { + if (item.get('id') === action.media.id) { + return fromJS(action.media); + } + + return item; + })); case COMPOSE_MENTION: return state.withMutations(map => { map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 560a7fffe..deaca7f3b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4877,6 +4877,15 @@ a.status-card.compact:hover { } } +.setting-divider { + background: transparent; + border: 0; + margin: 0; + width: 100%; + height: 1px; + margin-bottom: 29px; +} + .report-modal__comment { padding: 20px; border-right: 1px solid $ui-secondary-color; |