[Glitch] Add ability to choose media thumbnail in web UI
Port 825f73c3f9 to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
			
			
This commit is contained in:
		
							parent
							
								
									f0aee465d9
								
							
						
					
					
						commit
						409d00d256
					
				
					 4 changed files with 132 additions and 3 deletions
				
			
		|  | @ -30,6 +30,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'; | ||||||
|  | @ -289,6 +294,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()); | ||||||
|  | @ -307,6 +355,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 'flavours/glitch/actions/compose'; | import { changeUploadCompose, uploadThumbnail } from 'flavours/glitch/actions/compose'; | ||||||
| import { getPointerPosition } from 'flavours/glitch/features/video'; | import { getPointerPosition } from 'flavours/glitch/features/video'; | ||||||
| import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | ||||||
| import IconButton from 'flavours/glitch/components/icon_button'; | import IconButton from 'flavours/glitch/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'> | ||||||
|  |  | ||||||
|  | @ -15,6 +15,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, | ||||||
|  | @ -77,6 +81,8 @@ const initialState = ImmutableMap({ | ||||||
|   is_uploading: false, |   is_uploading: false, | ||||||
|   is_changing_upload: false, |   is_changing_upload: 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, | ||||||
|  | @ -433,6 +439,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(' ')); | ||||||
|  |  | ||||||
|  | @ -555,6 +555,15 @@ | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .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