mirror of
https://github.com/kikobar/mastodon.git
synced 2024-12-26 07:44:23 +00:00
Add ability to choose media thumbnail in web UI (#14244)
This commit is contained in:
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_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||||
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
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_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||||
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
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) {
|
export function changeUploadCompose(id, params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(changeUploadComposeRequest());
|
dispatch(changeUploadComposeRequest());
|
||||||
|
@ -278,6 +326,7 @@ export function changeUploadComposeRequest() {
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeUploadComposeSuccess(media) {
|
export function changeUploadComposeSuccess(media) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { changeUploadCompose } from '../../../actions/compose';
|
import { changeUploadCompose, uploadThumbnail } from '../../../actions/compose';
|
||||||
import { getPointerPosition } from '../../video';
|
import { getPointerPosition } from '../../video';
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import IconButton from 'mastodon/components/icon_button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
@ -23,11 +23,13 @@ const messages = defineMessages({
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
|
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
|
||||||
placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
|
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 }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
|
isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||||
|
@ -36,6 +38,10 @@ const mapDispatchToProps = (dispatch, { id }) => ({
|
||||||
dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
|
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, '******')
|
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
|
||||||
|
@ -81,6 +87,9 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
isUploadingThumbnail: PropTypes.bool,
|
||||||
|
onSave: PropTypes.func.isRequired,
|
||||||
|
onSelectThumbnail: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -235,13 +244,29 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||||
}).catch(() => this.setState({ detecting: false }));
|
}).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 () {
|
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 { x, y, dragging, description, dirty, detecting, progress } = this.state;
|
||||||
|
|
||||||
const width = media.getIn(['meta', 'original', 'width']) || null;
|
const width = media.getIn(['meta', 'original', 'width']) || null;
|
||||||
const height = media.getIn(['meta', 'original', 'height']) || null;
|
const height = media.getIn(['meta', 'original', 'height']) || null;
|
||||||
const focals = ['image', 'gifv'].includes(media.get('type'));
|
const focals = ['image', 'gifv'].includes(media.get('type'));
|
||||||
|
const thumbnailable = ['audio', 'video'].includes(media.get('type'));
|
||||||
|
|
||||||
const previewRatio = 16/9;
|
const previewRatio = 16/9;
|
||||||
const previewWidth = 200;
|
const previewWidth = 200;
|
||||||
|
@ -268,6 +293,30 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||||
<div className='report-modal__comment'>
|
<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>}
|
{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'>
|
<label className='setting-text-label' htmlFor='upload-modal__description'>
|
||||||
{descriptionLabel}
|
{descriptionLabel}
|
||||||
</label>
|
</label>
|
||||||
|
@ -293,7 +342,7 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||||
<CharacterCounter max={1500} text={detecting ? '' : description} />
|
<CharacterCounter max={1500} text={detecting ? '' : description} />
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className='focal-point-modal__content'>
|
<div className='focal-point-modal__content'>
|
||||||
|
|
|
@ -14,6 +14,10 @@ import {
|
||||||
COMPOSE_UPLOAD_FAIL,
|
COMPOSE_UPLOAD_FAIL,
|
||||||
COMPOSE_UPLOAD_UNDO,
|
COMPOSE_UPLOAD_UNDO,
|
||||||
COMPOSE_UPLOAD_PROGRESS,
|
COMPOSE_UPLOAD_PROGRESS,
|
||||||
|
THUMBNAIL_UPLOAD_REQUEST,
|
||||||
|
THUMBNAIL_UPLOAD_SUCCESS,
|
||||||
|
THUMBNAIL_UPLOAD_FAIL,
|
||||||
|
THUMBNAIL_UPLOAD_PROGRESS,
|
||||||
COMPOSE_SUGGESTIONS_CLEAR,
|
COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
COMPOSE_SUGGESTIONS_READY,
|
COMPOSE_SUGGESTIONS_READY,
|
||||||
COMPOSE_SUGGESTION_SELECT,
|
COMPOSE_SUGGESTION_SELECT,
|
||||||
|
@ -60,6 +64,8 @@ const initialState = ImmutableMap({
|
||||||
is_changing_upload: false,
|
is_changing_upload: false,
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
|
isUploadingThumbnail: false,
|
||||||
|
thumbnailProgress: 0,
|
||||||
media_attachments: ImmutableList(),
|
media_attachments: ImmutableList(),
|
||||||
pending_media_attachments: 0,
|
pending_media_attachments: 0,
|
||||||
poll: null,
|
poll: null,
|
||||||
|
@ -332,6 +338,22 @@ export default function compose(state = initialState, action) {
|
||||||
return removeMedia(state, action.media_id);
|
return removeMedia(state, action.media_id);
|
||||||
case COMPOSE_UPLOAD_PROGRESS:
|
case COMPOSE_UPLOAD_PROGRESS:
|
||||||
return state.set('progress', Math.round((action.loaded / action.total) * 100));
|
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:
|
case COMPOSE_MENTION:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
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 {
|
.report-modal__comment {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-right: 1px solid $ui-secondary-color;
|
border-right: 1px solid $ui-secondary-color;
|
||||||
|
|
Loading…
Reference in a new issue