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';
};
};
+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());
skipLoading: true,
};
};
+
export function changeUploadComposeSuccess(media) {
return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
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';
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 }) => ({
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, '******')
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,
};
}).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;
<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>
<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'>
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,
is_changing_upload: false,
is_uploading: false,
progress: 0,
+ isUploadingThumbnail: false,
+ thumbnailProgress: 0,
media_attachments: ImmutableList(),
pending_media_attachments: 0,
poll: null,
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(' '));