Add ability to choose media thumbnail in web UI (#14244)
parent
ed04697510
commit
06fc6a9cd4
|
@ -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,
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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(' '));
|
||||
|
|
|
@ -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;
|
||||
|
|
Reference in New Issue