Merge pull request #1570 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						359f8de2e8
					
				
					 27 changed files with 344 additions and 181 deletions
				
			
		
							
								
								
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							|  | @ -62,7 +62,7 @@ gem 'link_header', '~> 0.0' | |||
| gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar' | ||||
| gem 'nokogiri', '~> 1.11' | ||||
| gem 'nsa', '~> 0.2' | ||||
| gem 'oj', '~> 3.11' | ||||
| gem 'oj', '~> 3.12' | ||||
| gem 'ox', '~> 2.14' | ||||
| gem 'parslet' | ||||
| gem 'parallel', '~> 1.20' | ||||
|  |  | |||
							
								
								
									
										12
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Gemfile.lock
									
									
									
									
									
								
							|  | @ -301,7 +301,7 @@ GEM | |||
|       multi_json (~> 1.14) | ||||
|       rack (~> 2.0) | ||||
|       rdf (~> 3.1) | ||||
|     json-ld-preloaded (3.1.5) | ||||
|     json-ld-preloaded (3.1.6) | ||||
|       json-ld (~> 3.1) | ||||
|       rdf (~> 3.1) | ||||
|     jsonapi-renderer (0.2.2) | ||||
|  | @ -374,7 +374,7 @@ GEM | |||
|       concurrent-ruby (~> 1.0, >= 1.0.2) | ||||
|       sidekiq (>= 3.5) | ||||
|       statsd-ruby (~> 1.4, >= 1.4.0) | ||||
|     oj (3.11.8) | ||||
|     oj (3.12.1) | ||||
|     omniauth (1.9.1) | ||||
|       hashie (>= 3.4.6) | ||||
|       rack (>= 1.6.2, < 3) | ||||
|  | @ -401,7 +401,7 @@ GEM | |||
|     parallel (1.20.1) | ||||
|     parallel_tests (3.7.0) | ||||
|       parallel | ||||
|     parser (3.0.1.1) | ||||
|     parser (3.0.2.0) | ||||
|       ast (~> 2.4.1) | ||||
|     parslet (2.0.0) | ||||
|     pastel (0.8.0) | ||||
|  | @ -480,7 +480,7 @@ GEM | |||
|       thor (~> 1.0) | ||||
|     rainbow (3.0.0) | ||||
|     rake (13.0.3) | ||||
|     rdf (3.1.13) | ||||
|     rdf (3.1.15) | ||||
|       hamster (~> 3.0) | ||||
|       link_header (~> 0.0, >= 0.0.8) | ||||
|     rdf-normalize (0.4.0) | ||||
|  | @ -535,7 +535,7 @@ GEM | |||
|       unicode-display_width (>= 1.4.0, < 3.0) | ||||
|     rubocop-ast (1.7.0) | ||||
|       parser (>= 3.0.1.1) | ||||
|     rubocop-rails (2.11.2) | ||||
|     rubocop-rails (2.11.3) | ||||
|       activesupport (>= 4.2.0) | ||||
|       rack (>= 1.1) | ||||
|       rubocop (>= 1.7.0, < 2.0) | ||||
|  | @ -734,7 +734,7 @@ DEPENDENCIES | |||
|   net-ldap (~> 0.17) | ||||
|   nokogiri (~> 1.11) | ||||
|   nsa (~> 0.2) | ||||
|   oj (~> 3.11) | ||||
|   oj (~> 3.12) | ||||
|   omniauth (~> 1.9) | ||||
|   omniauth-cas (~> 2.0) | ||||
|   omniauth-rails_csrf_protection (~> 0.1) | ||||
|  |  | |||
|  | @ -40,7 +40,12 @@ class Api::BaseController < ApplicationController | |||
|     render json: { error: 'This action is not allowed' }, status: 403 | ||||
|   end | ||||
| 
 | ||||
|   rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight do | ||||
|   rescue_from Seahorse::Client::NetworkingError do |e| | ||||
|     Rails.logger.warn "Storage server error: #{e}" | ||||
|     render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 | ||||
|   end | ||||
| 
 | ||||
|   rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do | ||||
|     render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,12 @@ class ApplicationController < ActionController::Base | |||
|   rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests | ||||
| 
 | ||||
|   rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error | ||||
|   rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable | ||||
|   rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable | ||||
| 
 | ||||
|   rescue_from Seahorse::Client::NetworkingError do |e| | ||||
|     Rails.logger.warn "Storage server error: #{e}" | ||||
|     service_unavailable | ||||
|   end | ||||
| 
 | ||||
|   before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? | ||||
|   before_action :require_functional!, if: :user_signed_in? | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import { importFetchedAccounts } from './importer'; | |||
| import { updateTimeline } from './timelines'; | ||||
| import { showAlertForError } from './alerts'; | ||||
| import { showAlert } from './alerts'; | ||||
| import { openModal } from './modal'; | ||||
| import { defineMessages } from 'react-intl'; | ||||
| 
 | ||||
| let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; | ||||
|  | @ -68,6 +69,11 @@ export const COMPOSE_POLL_OPTION_CHANGE   = 'COMPOSE_POLL_OPTION_CHANGE'; | |||
| export const COMPOSE_POLL_OPTION_REMOVE   = 'COMPOSE_POLL_OPTION_REMOVE'; | ||||
| export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; | ||||
| 
 | ||||
| export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; | ||||
| 
 | ||||
| export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION'; | ||||
| export const COMPOSE_CHANGE_MEDIA_FOCUS       = 'COMPOSE_CHANGE_MEDIA_FOCUS'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, | ||||
|   uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, | ||||
|  | @ -339,6 +345,32 @@ export const uploadThumbnailFail = error => ({ | |||
|   skipLoading: true, | ||||
| }); | ||||
| 
 | ||||
| export function initMediaEditModal(id) { | ||||
|   return dispatch => { | ||||
|     dispatch({ | ||||
|       type: INIT_MEDIA_EDIT_MODAL, | ||||
|       id, | ||||
|     }); | ||||
| 
 | ||||
|     dispatch(openModal('FOCAL_POINT', { id })); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function onChangeMediaDescription(description) { | ||||
|   return { | ||||
|     type: COMPOSE_CHANGE_MEDIA_DESCRIPTION, | ||||
|     description, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function onChangeMediaFocus(focusX, focusY) { | ||||
|   return { | ||||
|     type: COMPOSE_CHANGE_MEDIA_FOCUS, | ||||
|     focusX, | ||||
|     focusY, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeUploadCompose(id, params) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(changeUploadComposeRequest()); | ||||
|  |  | |||
|  | @ -543,9 +543,8 @@ class Status extends ImmutablePureComponent { | |||
|       return ( | ||||
|         <HotKeys handlers={handlers}> | ||||
|           <div ref={this.handleRef} className='status focusable' tabIndex='0'> | ||||
|             {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} | ||||
|             {' '} | ||||
|             {status.get('content')} | ||||
|             <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span> | ||||
|             <span>{status.get('content')}</span> | ||||
|           </div> | ||||
|         </HotKeys> | ||||
|       ); | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import Upload from '../components/upload'; | ||||
| import { undoUploadCompose } from 'flavours/glitch/actions/compose'; | ||||
| import { openModal } from 'flavours/glitch/actions/modal'; | ||||
| import { undoUploadCompose, initMediaEditModal } from 'flavours/glitch/actions/compose'; | ||||
| import { submitCompose } from 'flavours/glitch/actions/compose'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { id }) => ({ | ||||
|  | @ -15,7 +14,7 @@ const mapDispatchToProps = dispatch => ({ | |||
|   }, | ||||
| 
 | ||||
|   onOpenFocalPoint: id => { | ||||
|     dispatch(openModal('FOCAL_POINT', { id })); | ||||
|     dispatch(initMediaEditModal(id)); | ||||
|   }, | ||||
| 
 | ||||
|   onSubmit (router) { | ||||
|  |  | |||
|  | @ -116,7 +116,11 @@ class Footer extends ImmutablePureComponent { | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const { status } = this.props; | ||||
|     const { status, onClose } = this.props; | ||||
| 
 | ||||
|     if (onClose) { | ||||
|       onClose(); | ||||
|     } | ||||
| 
 | ||||
|     router.history.push(`/statuses/${status.get('id')}`); | ||||
|   } | ||||
|  |  | |||
|  | @ -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, uploadThumbnail } from 'flavours/glitch/actions/compose'; | ||||
| import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from 'flavours/glitch/actions/compose'; | ||||
| import { getPointerPosition } from 'flavours/glitch/features/video'; | ||||
| import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | ||||
| import IconButton from 'flavours/glitch/components/icon_button'; | ||||
|  | @ -27,14 +27,22 @@ import { assetHost } from 'flavours/glitch/util/config'; | |||
| const messages = defineMessages({ | ||||
|   close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
|   apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' }, | ||||
|   applying: { id: 'upload_modal.applying', defaultMessage: 'Applying…' }, | ||||
|   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' }, | ||||
|   discardMessage: { id: 'confirmations.discard_edit_media.message', defaultMessage: 'You have unsaved changes to the media description or preview, discard them anyway?' }, | ||||
|   discardConfirm: { id: 'confirmations.discard_edit_media.confirm', defaultMessage: 'Discard' }, | ||||
| }); | ||||
| 
 | ||||
| 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']), | ||||
|   description: state.getIn(['compose', 'media_modal', 'description']), | ||||
|   focusX: state.getIn(['compose', 'media_modal', 'focusX']), | ||||
|   focusY: state.getIn(['compose', 'media_modal', 'focusY']), | ||||
|   dirty: state.getIn(['compose', 'media_modal', 'dirty']), | ||||
|   is_changing_upload: state.getIn(['compose', 'is_changing_upload']), | ||||
| }); | ||||
| 
 | ||||
| const mapDispatchToProps = (dispatch, { id }) => ({ | ||||
|  | @ -43,6 +51,14 @@ const mapDispatchToProps = (dispatch, { id }) => ({ | |||
|     dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); | ||||
|   }, | ||||
| 
 | ||||
|   onChangeDescription: (description) => { | ||||
|     dispatch(onChangeMediaDescription(description)); | ||||
|   }, | ||||
| 
 | ||||
|   onChangeFocus: (focusX, focusY) => { | ||||
|     dispatch(onChangeMediaFocus(focusX, focusY)); | ||||
|   }, | ||||
| 
 | ||||
|   onSelectThumbnail: files => { | ||||
|     dispatch(uploadThumbnail(id, files[0])); | ||||
|   }, | ||||
|  | @ -83,8 +99,8 @@ class ImageLoader extends React.PureComponent { | |||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default @connect(mapStateToProps, mapDispatchToProps) | ||||
| @injectIntl | ||||
| export default @connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true }) | ||||
| @(component => injectIntl(component, { withRef: true })) | ||||
| class FocalPointModal extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|  | @ -92,34 +108,21 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     isUploadingThumbnail: PropTypes.bool, | ||||
|     onSave: PropTypes.func.isRequired, | ||||
|     onChangeDescription: PropTypes.func.isRequired, | ||||
|     onChangeFocus: PropTypes.func.isRequired, | ||||
|     onSelectThumbnail: PropTypes.func.isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     x: 0, | ||||
|     y: 0, | ||||
|     focusX: 0, | ||||
|     focusY: 0, | ||||
|     dragging: false, | ||||
|     description: '', | ||||
|     dirty: false, | ||||
|     progress: 0, | ||||
|     loading: true, | ||||
|     ocrStatus: '', | ||||
|   }; | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.updatePositionFromMedia(this.props.media); | ||||
|   } | ||||
| 
 | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (this.props.media.get('id') !== nextProps.media.get('id')) { | ||||
|       this.updatePositionFromMedia(nextProps.media); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     document.removeEventListener('mousemove', this.handleMouseMove); | ||||
|     document.removeEventListener('mouseup', this.handleMouseUp); | ||||
|  | @ -164,54 +167,37 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|     const focusX   = (x - .5) *  2; | ||||
|     const focusY   = (y - .5) * -2; | ||||
| 
 | ||||
|     this.setState({ x, y, focusX, focusY, dirty: true }); | ||||
|   } | ||||
| 
 | ||||
|   updatePositionFromMedia = media => { | ||||
|     const focusX      = media.getIn(['meta', 'focus', 'x']); | ||||
|     const focusY      = media.getIn(['meta', 'focus', 'y']); | ||||
|     const description = media.get('description') || ''; | ||||
| 
 | ||||
|     if (focusX && focusY) { | ||||
|       const x = (focusX /  2) + .5; | ||||
|       const y = (focusY / -2) + .5; | ||||
| 
 | ||||
|       this.setState({ | ||||
|         x, | ||||
|         y, | ||||
|         focusX, | ||||
|         focusY, | ||||
|         description, | ||||
|         dirty: false, | ||||
|       }); | ||||
|     } else { | ||||
|       this.setState({ | ||||
|         x: 0.5, | ||||
|         y: 0.5, | ||||
|         focusX: 0, | ||||
|         focusY: 0, | ||||
|         description, | ||||
|         dirty: false, | ||||
|       }); | ||||
|     } | ||||
|     this.props.onChangeFocus(focusX, focusY); | ||||
|   } | ||||
| 
 | ||||
|   handleChange = e => { | ||||
|     this.setState({ description: e.target.value, dirty: true }); | ||||
|     this.props.onChangeDescription(e.target.value); | ||||
|   } | ||||
| 
 | ||||
|   handleKeyDown = (e) => { | ||||
|     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { | ||||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|       this.setState({ description: e.target.value, dirty: true }); | ||||
|       this.props.onChangeDescription(e.target.value); | ||||
|       this.handleSubmit(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleSubmit = () => { | ||||
|     this.props.onSave(this.state.description, this.state.focusX, this.state.focusY); | ||||
|     this.props.onClose(); | ||||
|     this.props.onSave(this.props.description, this.props.focusX, this.props.focusY); | ||||
|   } | ||||
| 
 | ||||
|   getCloseConfirmationMessage = () => { | ||||
|     const { intl, dirty } = this.props; | ||||
| 
 | ||||
|     if (dirty) { | ||||
|       return { | ||||
|         message: intl.formatMessage(messages.discardMessage), | ||||
|         confirm: intl.formatMessage(messages.discardConfirm), | ||||
|       }; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|  | @ -257,7 +243,8 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|         await worker.loadLanguage('eng'); | ||||
|         await worker.initialize('eng'); | ||||
|         const { data: { text } } = await worker.recognize(media_url); | ||||
|         this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }); | ||||
|         this.setState({ detecting: false }); | ||||
|         this.props.onChangeDescription(removeExtraLineBreaks(text)); | ||||
|         await worker.terminate(); | ||||
|       })().catch((e) => { | ||||
|         if (refreshCache) { | ||||
|  | @ -274,7 +261,6 @@ class FocalPointModal extends ImmutablePureComponent { | |||
| 
 | ||||
|   handleThumbnailChange = e => { | ||||
|     if (e.target.files.length > 0) { | ||||
|       this.setState({ dirty: true }); | ||||
|       this.props.onSelectThumbnail(e.target.files); | ||||
|     } | ||||
|   } | ||||
|  | @ -288,8 +274,10 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { media, intl, account, onClose, isUploadingThumbnail } = this.props; | ||||
|     const { x, y, dragging, description, dirty, detecting, progress, ocrStatus } = this.state; | ||||
|     const { media, intl, account, onClose, isUploadingThumbnail, description, focusX, focusY, dirty, is_changing_upload } = this.props; | ||||
|     const { dragging, detecting, progress, ocrStatus } = this.state; | ||||
|     const x = (focusX /  2) + .5; | ||||
|     const y = (focusY / -2) + .5; | ||||
| 
 | ||||
|     const width  = media.getIn(['meta', 'original', 'width']) || null; | ||||
|     const height = media.getIn(['meta', 'original', 'height']) || null; | ||||
|  | @ -344,7 +332,7 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|                     accept='image/png,image/jpeg' | ||||
|                     onChange={this.handleThumbnailChange} | ||||
|                     style={{ display: 'none' }} | ||||
|                     disabled={isUploadingThumbnail} | ||||
|                     disabled={isUploadingThumbnail || is_changing_upload} | ||||
|                   /> | ||||
|                 </label> | ||||
| 
 | ||||
|  | @ -363,7 +351,7 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|                 value={detecting ? '…' : description} | ||||
|                 onChange={this.handleChange} | ||||
|                 onKeyDown={this.handleKeyDown} | ||||
|                 disabled={detecting} | ||||
|                 disabled={detecting || is_changing_upload} | ||||
|                 autoFocus | ||||
|               /> | ||||
| 
 | ||||
|  | @ -373,11 +361,11 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|             </div> | ||||
| 
 | ||||
|             <div className='setting-text__toolbar'> | ||||
|               <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> | ||||
|               <button disabled={detecting || media.get('type') !== 'image' || is_changing_upload} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> | ||||
|               <CharacterCounter max={1500} text={detecting ? '' : description} /> | ||||
|             </div> | ||||
| 
 | ||||
|             <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> | ||||
|             <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500 || is_changing_upload} text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)} onClick={this.handleSubmit} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className='focal-point-modal__content'> | ||||
|  |  | |||
|  | @ -83,16 +83,33 @@ export default class ModalRoot extends React.PureComponent { | |||
|     return <BundleModalError {...props} onClose={onClose} />; | ||||
|   } | ||||
| 
 | ||||
|   handleClose = () => { | ||||
|     const { onClose } = this.props; | ||||
|     let message = null; | ||||
|     try { | ||||
|       message = this._modal?.getWrappedInstance?.().getCloseConfirmationMessage?.(); | ||||
|     } catch (_) { | ||||
|       // injectIntl defines `getWrappedInstance` but errors out if `withRef`
 | ||||
|       // isn't set.
 | ||||
|       // This would be much smoother with react-intl 3+ and `forwardRef`.
 | ||||
|     } | ||||
|     onClose(message); | ||||
|   } | ||||
| 
 | ||||
|   setModalRef = (c) => { | ||||
|     this._modal = c; | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { type, props, onClose } = this.props; | ||||
|     const { type, props } = this.props; | ||||
|     const { backgroundColor } = this.state; | ||||
|     const visible = !!type; | ||||
| 
 | ||||
|     return ( | ||||
|       <Base backgroundColor={backgroundColor} onClose={onClose} noEsc={props ? props.noEsc : false}> | ||||
|       <Base backgroundColor={backgroundColor} onClose={this.handleClose} noEsc={props ? props.noEsc : false}> | ||||
|         {visible && ( | ||||
|           <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> | ||||
|             {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={onClose} />} | ||||
|             {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />} | ||||
|           </BundleContainer> | ||||
|         )} | ||||
|       </Base> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import { closeModal } from 'flavours/glitch/actions/modal'; | ||||
| import { openModal, closeModal } from 'flavours/glitch/actions/modal'; | ||||
| import ModalRoot from '../components/modal_root'; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|  | @ -8,8 +8,18 @@ const mapStateToProps = state => ({ | |||
| }); | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|   onClose () { | ||||
|   onClose (confirmationMessage) { | ||||
|     if (confirmationMessage) { | ||||
|       dispatch( | ||||
|         openModal('CONFIRM', { | ||||
|           message: confirmationMessage.message, | ||||
|           confirm: confirmationMessage.confirm, | ||||
|           onConfirm: () => dispatch(closeModal()), | ||||
|         }), | ||||
|       ); | ||||
|     } else { | ||||
|       dispatch(closeModal()); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -42,6 +42,9 @@ import { | |||
|   COMPOSE_POLL_OPTION_CHANGE, | ||||
|   COMPOSE_POLL_OPTION_REMOVE, | ||||
|   COMPOSE_POLL_SETTINGS_CHANGE, | ||||
|   INIT_MEDIA_EDIT_MODAL, | ||||
|   COMPOSE_CHANGE_MEDIA_DESCRIPTION, | ||||
|   COMPOSE_CHANGE_MEDIA_FOCUS, | ||||
| } from 'flavours/glitch/actions/compose'; | ||||
| import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines'; | ||||
| import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; | ||||
|  | @ -97,6 +100,13 @@ const initialState = ImmutableMap({ | |||
|   resetFileKey: Math.floor((Math.random() * 0x10000)), | ||||
|   idempotencyKey: null, | ||||
|   tagHistory: ImmutableList(), | ||||
|   media_modal: ImmutableMap({ | ||||
|     id: null, | ||||
|     description: '', | ||||
|     focusX: 0, | ||||
|     focusY: 0, | ||||
|     dirty: false, | ||||
|   }), | ||||
|   doodle: ImmutableMap({ | ||||
|     fg: 'rgb(  0,    0,    0)', | ||||
|     bg: 'rgb(255,  255,  255)', | ||||
|  | @ -455,6 +465,19 @@ export default function compose(state = initialState, action) { | |||
| 
 | ||||
|         return item; | ||||
|       })); | ||||
|   case INIT_MEDIA_EDIT_MODAL: | ||||
|     const media =  state.get('media_attachments').find(item => item.get('id') === action.id); | ||||
|     return state.set('media_modal', ImmutableMap({ | ||||
|       id: action.id, | ||||
|       description: media.get('description') || '', | ||||
|       focusX: media.getIn(['meta', 'focus', 'x'], 0), | ||||
|       focusY: media.getIn(['meta', 'focus', 'y'], 0), | ||||
|       dirty: false, | ||||
|     })); | ||||
|   case COMPOSE_CHANGE_MEDIA_DESCRIPTION: | ||||
|     return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true); | ||||
|   case COMPOSE_CHANGE_MEDIA_FOCUS: | ||||
|     return state.setIn(['media_modal', 'focusX'], action.focusX).setIn(['media_modal', 'focusY'], action.focusY).setIn(['media_modal', 'dirty'], true); | ||||
|   case COMPOSE_MENTION: | ||||
|     return state.withMutations(map => { | ||||
|       map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); | ||||
|  | @ -491,6 +514,7 @@ export default function compose(state = initialState, action) { | |||
|   case COMPOSE_UPLOAD_CHANGE_SUCCESS: | ||||
|     return state | ||||
|       .set('is_changing_upload', false) | ||||
|       .setIn(['media_modal', 'dirty'], false) | ||||
|       .update('media_attachments', list => list.map(item => { | ||||
|         if (item.get('id') === action.media.id) { | ||||
|           return fromJS(action.media); | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal'; | ||||
| import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines'; | ||||
| import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from 'flavours/glitch/actions/compose'; | ||||
| import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable'; | ||||
| 
 | ||||
| export default function modal(state = ImmutableStack(), action) { | ||||
|  | @ -8,6 +9,8 @@ export default function modal(state = ImmutableStack(), action) { | |||
|     return state.unshift(ImmutableMap({ modalType: action.modalType, modalProps: action.modalProps })); | ||||
|   case MODAL_CLOSE: | ||||
|     return (action.modalType === undefined || action.modalType === state.getIn([0, 'modalType'])) ? state.shift() : state; | ||||
|   case COMPOSE_UPLOAD_CHANGE_SUCCESS: | ||||
|     return state.getIn([0, 'modalType']) === 'FOCAL_POINT' ? state.shift() : state; | ||||
|   case TIMELINE_DELETE: | ||||
|     return state.filterNot((modal) => modal.get('modalProps').statusId === action.id); | ||||
|   default: | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { importFetchedAccounts } from './importer'; | |||
| import { updateTimeline } from './timelines'; | ||||
| import { showAlertForError } from './alerts'; | ||||
| import { showAlert } from './alerts'; | ||||
| import { openModal } from './modal'; | ||||
| import { defineMessages } from 'react-intl'; | ||||
| 
 | ||||
| let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; | ||||
|  | @ -63,6 +64,11 @@ export const COMPOSE_POLL_OPTION_CHANGE   = 'COMPOSE_POLL_OPTION_CHANGE'; | |||
| export const COMPOSE_POLL_OPTION_REMOVE   = 'COMPOSE_POLL_OPTION_REMOVE'; | ||||
| export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; | ||||
| 
 | ||||
| export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; | ||||
| 
 | ||||
| export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION'; | ||||
| export const COMPOSE_CHANGE_MEDIA_FOCUS       = 'COMPOSE_CHANGE_MEDIA_FOCUS'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, | ||||
|   uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, | ||||
|  | @ -308,6 +314,32 @@ export const uploadThumbnailFail = error => ({ | |||
|   skipLoading: true, | ||||
| }); | ||||
| 
 | ||||
| export function initMediaEditModal(id) { | ||||
|   return dispatch => { | ||||
|     dispatch({ | ||||
|       type: INIT_MEDIA_EDIT_MODAL, | ||||
|       id, | ||||
|     }); | ||||
| 
 | ||||
|     dispatch(openModal('FOCAL_POINT', { id })); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function onChangeMediaDescription(description) { | ||||
|   return { | ||||
|     type: COMPOSE_CHANGE_MEDIA_DESCRIPTION, | ||||
|     description, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function onChangeMediaFocus(focusX, focusY) { | ||||
|   return { | ||||
|     type: COMPOSE_CHANGE_MEDIA_FOCUS, | ||||
|     focusX, | ||||
|     focusY, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeUploadCompose(id, params) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(changeUploadComposeRequest()); | ||||
|  |  | |||
|  | @ -309,8 +309,8 @@ class Status extends ImmutablePureComponent { | |||
|       return ( | ||||
|         <HotKeys handlers={handlers}> | ||||
|           <div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'> | ||||
|             {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} | ||||
|             {status.get('content')} | ||||
|             <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span> | ||||
|             <span>{status.get('content')}</span> | ||||
|           </div> | ||||
|         </HotKeys> | ||||
|       ); | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import Upload from '../components/upload'; | ||||
| import { undoUploadCompose } from '../../../actions/compose'; | ||||
| import { openModal } from '../../../actions/modal'; | ||||
| import { undoUploadCompose, initMediaEditModal } from '../../../actions/compose'; | ||||
| import { submitCompose } from '../../../actions/compose'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { id }) => ({ | ||||
|  | @ -15,7 +14,7 @@ const mapDispatchToProps = dispatch => ({ | |||
|   }, | ||||
| 
 | ||||
|   onOpenFocalPoint: id => { | ||||
|     dispatch(openModal('FOCAL_POINT', { id })); | ||||
|     dispatch(initMediaEditModal(id)); | ||||
|   }, | ||||
| 
 | ||||
|   onSubmit (router) { | ||||
|  |  | |||
|  | @ -114,7 +114,11 @@ class Footer extends ImmutablePureComponent { | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const { status } = this.props; | ||||
|     const { status, onClose } = this.props; | ||||
| 
 | ||||
|     if (onClose) { | ||||
|       onClose(); | ||||
|     } | ||||
| 
 | ||||
|     router.history.push(`/statuses/${status.get('id')}`); | ||||
|   } | ||||
|  |  | |||
|  | @ -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, uploadThumbnail } from '../../../actions/compose'; | ||||
| import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose'; | ||||
| import { getPointerPosition } from '../../video'; | ||||
| import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | ||||
| import IconButton from 'mastodon/components/icon_button'; | ||||
|  | @ -27,14 +27,22 @@ import { assetHost } from 'mastodon/utils/config'; | |||
| const messages = defineMessages({ | ||||
|   close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
|   apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' }, | ||||
|   applying: { id: 'upload_modal.applying', defaultMessage: 'Applying…' }, | ||||
|   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' }, | ||||
|   discardMessage: { id: 'confirmations.discard_edit_media.message', defaultMessage: 'You have unsaved changes to the media description or preview, discard them anyway?' }, | ||||
|   discardConfirm: { id: 'confirmations.discard_edit_media.confirm', defaultMessage: 'Discard' }, | ||||
| }); | ||||
| 
 | ||||
| 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']), | ||||
|   description: state.getIn(['compose', 'media_modal', 'description']), | ||||
|   focusX: state.getIn(['compose', 'media_modal', 'focusX']), | ||||
|   focusY: state.getIn(['compose', 'media_modal', 'focusY']), | ||||
|   dirty: state.getIn(['compose', 'media_modal', 'dirty']), | ||||
|   is_changing_upload: state.getIn(['compose', 'is_changing_upload']), | ||||
| }); | ||||
| 
 | ||||
| const mapDispatchToProps = (dispatch, { id }) => ({ | ||||
|  | @ -43,6 +51,14 @@ const mapDispatchToProps = (dispatch, { id }) => ({ | |||
|     dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); | ||||
|   }, | ||||
| 
 | ||||
|   onChangeDescription: (description) => { | ||||
|     dispatch(onChangeMediaDescription(description)); | ||||
|   }, | ||||
| 
 | ||||
|   onChangeFocus: (focusX, focusY) => { | ||||
|     dispatch(onChangeMediaFocus(focusX, focusY)); | ||||
|   }, | ||||
| 
 | ||||
|   onSelectThumbnail: files => { | ||||
|     dispatch(uploadThumbnail(id, files[0])); | ||||
|   }, | ||||
|  | @ -83,8 +99,8 @@ class ImageLoader extends React.PureComponent { | |||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default @connect(mapStateToProps, mapDispatchToProps) | ||||
| @injectIntl | ||||
| export default @connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true }) | ||||
| @(component => injectIntl(component, { withRef: true })) | ||||
| class FocalPointModal extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|  | @ -92,34 +108,21 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     isUploadingThumbnail: PropTypes.bool, | ||||
|     onSave: PropTypes.func.isRequired, | ||||
|     onChangeDescription: PropTypes.func.isRequired, | ||||
|     onChangeFocus: PropTypes.func.isRequired, | ||||
|     onSelectThumbnail: PropTypes.func.isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     x: 0, | ||||
|     y: 0, | ||||
|     focusX: 0, | ||||
|     focusY: 0, | ||||
|     dragging: false, | ||||
|     description: '', | ||||
|     dirty: false, | ||||
|     progress: 0, | ||||
|     loading: true, | ||||
|     ocrStatus: '', | ||||
|   }; | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.updatePositionFromMedia(this.props.media); | ||||
|   } | ||||
| 
 | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (this.props.media.get('id') !== nextProps.media.get('id')) { | ||||
|       this.updatePositionFromMedia(nextProps.media); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     document.removeEventListener('mousemove', this.handleMouseMove); | ||||
|     document.removeEventListener('mouseup', this.handleMouseUp); | ||||
|  | @ -164,54 +167,37 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|     const focusX   = (x - .5) *  2; | ||||
|     const focusY   = (y - .5) * -2; | ||||
| 
 | ||||
|     this.setState({ x, y, focusX, focusY, dirty: true }); | ||||
|   } | ||||
| 
 | ||||
|   updatePositionFromMedia = media => { | ||||
|     const focusX      = media.getIn(['meta', 'focus', 'x']); | ||||
|     const focusY      = media.getIn(['meta', 'focus', 'y']); | ||||
|     const description = media.get('description') || ''; | ||||
| 
 | ||||
|     if (focusX && focusY) { | ||||
|       const x = (focusX /  2) + .5; | ||||
|       const y = (focusY / -2) + .5; | ||||
| 
 | ||||
|       this.setState({ | ||||
|         x, | ||||
|         y, | ||||
|         focusX, | ||||
|         focusY, | ||||
|         description, | ||||
|         dirty: false, | ||||
|       }); | ||||
|     } else { | ||||
|       this.setState({ | ||||
|         x: 0.5, | ||||
|         y: 0.5, | ||||
|         focusX: 0, | ||||
|         focusY: 0, | ||||
|         description, | ||||
|         dirty: false, | ||||
|       }); | ||||
|     } | ||||
|     this.props.onChangeFocus(focusX, focusY); | ||||
|   } | ||||
| 
 | ||||
|   handleChange = e => { | ||||
|     this.setState({ description: e.target.value, dirty: true }); | ||||
|     this.props.onChangeDescription(e.target.value); | ||||
|   } | ||||
| 
 | ||||
|   handleKeyDown = (e) => { | ||||
|     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { | ||||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|       this.setState({ description: e.target.value, dirty: true }); | ||||
|       this.props.onChangeDescription(e.target.value); | ||||
|       this.handleSubmit(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleSubmit = () => { | ||||
|     this.props.onSave(this.state.description, this.state.focusX, this.state.focusY); | ||||
|     this.props.onClose(); | ||||
|     this.props.onSave(this.props.description, this.props.focusX, this.props.focusY); | ||||
|   } | ||||
| 
 | ||||
|   getCloseConfirmationMessage = () => { | ||||
|     const { intl, dirty } = this.props; | ||||
| 
 | ||||
|     if (dirty) { | ||||
|       return { | ||||
|         message: intl.formatMessage(messages.discardMessage), | ||||
|         confirm: intl.formatMessage(messages.discardConfirm), | ||||
|       }; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|  | @ -257,7 +243,8 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|         await worker.loadLanguage('eng'); | ||||
|         await worker.initialize('eng'); | ||||
|         const { data: { text } } = await worker.recognize(media_url); | ||||
|         this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }); | ||||
|         this.setState({ detecting: false }); | ||||
|         this.props.onChangeDescription(removeExtraLineBreaks(text)); | ||||
|         await worker.terminate(); | ||||
|       })().catch((e) => { | ||||
|         if (refreshCache) { | ||||
|  | @ -274,7 +261,6 @@ class FocalPointModal extends ImmutablePureComponent { | |||
| 
 | ||||
|   handleThumbnailChange = e => { | ||||
|     if (e.target.files.length > 0) { | ||||
|       this.setState({ dirty: true }); | ||||
|       this.props.onSelectThumbnail(e.target.files); | ||||
|     } | ||||
|   } | ||||
|  | @ -288,8 +274,10 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { media, intl, account, onClose, isUploadingThumbnail } = this.props; | ||||
|     const { x, y, dragging, description, dirty, detecting, progress, ocrStatus } = this.state; | ||||
|     const { media, intl, account, onClose, isUploadingThumbnail, description, focusX, focusY, dirty, is_changing_upload } = this.props; | ||||
|     const { dragging, detecting, progress, ocrStatus } = this.state; | ||||
|     const x = (focusX /  2) + .5; | ||||
|     const y = (focusY / -2) + .5; | ||||
| 
 | ||||
|     const width  = media.getIn(['meta', 'original', 'width']) || null; | ||||
|     const height = media.getIn(['meta', 'original', 'height']) || null; | ||||
|  | @ -344,7 +332,7 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|                     accept='image/png,image/jpeg' | ||||
|                     onChange={this.handleThumbnailChange} | ||||
|                     style={{ display: 'none' }} | ||||
|                     disabled={isUploadingThumbnail} | ||||
|                     disabled={isUploadingThumbnail || is_changing_upload} | ||||
|                   /> | ||||
|                 </label> | ||||
| 
 | ||||
|  | @ -363,7 +351,7 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|                 value={detecting ? '…' : description} | ||||
|                 onChange={this.handleChange} | ||||
|                 onKeyDown={this.handleKeyDown} | ||||
|                 disabled={detecting} | ||||
|                 disabled={detecting || is_changing_upload} | ||||
|                 autoFocus | ||||
|               /> | ||||
| 
 | ||||
|  | @ -373,11 +361,11 @@ class FocalPointModal extends ImmutablePureComponent { | |||
|             </div> | ||||
| 
 | ||||
|             <div className='setting-text__toolbar'> | ||||
|               <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> | ||||
|               <button disabled={detecting || media.get('type') !== 'image' || is_changing_upload} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> | ||||
|               <CharacterCounter max={1500} text={detecting ? '' : description} /> | ||||
|             </div> | ||||
| 
 | ||||
|             <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> | ||||
|             <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500 || is_changing_upload} text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)} onClick={this.handleSubmit} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className='focal-point-modal__content'> | ||||
|  |  | |||
|  | @ -77,16 +77,33 @@ export default class ModalRoot extends React.PureComponent { | |||
|     return <BundleModalError {...props} onClose={onClose} />; | ||||
|   } | ||||
| 
 | ||||
|   handleClose = () => { | ||||
|     const { onClose } = this.props; | ||||
|     let message = null; | ||||
|     try { | ||||
|       message = this._modal?.getWrappedInstance?.().getCloseConfirmationMessage?.(); | ||||
|     } catch (_) { | ||||
|       // injectIntl defines `getWrappedInstance` but errors out if `withRef`
 | ||||
|       // isn't set.
 | ||||
|       // This would be much smoother with react-intl 3+ and `forwardRef`.
 | ||||
|     } | ||||
|     onClose(message); | ||||
|   } | ||||
| 
 | ||||
|   setModalRef = (c) => { | ||||
|     this._modal = c; | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { type, props, onClose } = this.props; | ||||
|     const { type, props } = this.props; | ||||
|     const { backgroundColor } = this.state; | ||||
|     const visible = !!type; | ||||
| 
 | ||||
|     return ( | ||||
|       <Base backgroundColor={backgroundColor} onClose={onClose}> | ||||
|       <Base backgroundColor={backgroundColor} onClose={this.handleClose}> | ||||
|         {visible && ( | ||||
|           <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> | ||||
|             {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={onClose} />} | ||||
|             {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />} | ||||
|           </BundleContainer> | ||||
|         )} | ||||
|       </Base> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import { closeModal } from '../../../actions/modal'; | ||||
| import { openModal, closeModal } from '../../../actions/modal'; | ||||
| import ModalRoot from '../components/modal_root'; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|  | @ -8,8 +8,18 @@ const mapStateToProps = state => ({ | |||
| }); | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|   onClose () { | ||||
|   onClose (confirmationMessage) { | ||||
|     if (confirmationMessage) { | ||||
|       dispatch( | ||||
|         openModal('CONFIRM', { | ||||
|           message: confirmationMessage.message, | ||||
|           confirm: confirmationMessage.confirm, | ||||
|           onConfirm: () => dispatch(closeModal()), | ||||
|         }), | ||||
|       ); | ||||
|     } else { | ||||
|       dispatch(closeModal()); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,6 +39,9 @@ import { | |||
|   COMPOSE_POLL_OPTION_CHANGE, | ||||
|   COMPOSE_POLL_OPTION_REMOVE, | ||||
|   COMPOSE_POLL_SETTINGS_CHANGE, | ||||
|   INIT_MEDIA_EDIT_MODAL, | ||||
|   COMPOSE_CHANGE_MEDIA_DESCRIPTION, | ||||
|   COMPOSE_CHANGE_MEDIA_FOCUS, | ||||
| } from '../actions/compose'; | ||||
| import { TIMELINE_DELETE } from '../actions/timelines'; | ||||
| import { STORE_HYDRATE } from '../actions/store'; | ||||
|  | @ -76,6 +79,13 @@ const initialState = ImmutableMap({ | |||
|   resetFileKey: Math.floor((Math.random() * 0x10000)), | ||||
|   idempotencyKey: null, | ||||
|   tagHistory: ImmutableList(), | ||||
|   media_modal: ImmutableMap({ | ||||
|     id: null, | ||||
|     description: '', | ||||
|     focusX: 0, | ||||
|     focusY: 0, | ||||
|     dirty: false, | ||||
|   }), | ||||
| }); | ||||
| 
 | ||||
| const initialPoll = ImmutableMap({ | ||||
|  | @ -354,6 +364,19 @@ export default function compose(state = initialState, action) { | |||
| 
 | ||||
|         return item; | ||||
|       })); | ||||
|   case INIT_MEDIA_EDIT_MODAL: | ||||
|     const media =  state.get('media_attachments').find(item => item.get('id') === action.id); | ||||
|     return state.set('media_modal', ImmutableMap({ | ||||
|       id: action.id, | ||||
|       description: media.get('description') || '', | ||||
|       focusX: media.getIn(['meta', 'focus', 'x'], 0), | ||||
|       focusY: media.getIn(['meta', 'focus', 'y'], 0), | ||||
|       dirty: false, | ||||
|     })); | ||||
|   case COMPOSE_CHANGE_MEDIA_DESCRIPTION: | ||||
|     return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true); | ||||
|   case COMPOSE_CHANGE_MEDIA_FOCUS: | ||||
|     return state.setIn(['media_modal', 'focusX'], action.focusX).setIn(['media_modal', 'focusY'], action.focusY).setIn(['media_modal', 'dirty'], true); | ||||
|   case COMPOSE_MENTION: | ||||
|     return state.withMutations(map => { | ||||
|       map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); | ||||
|  | @ -390,6 +413,7 @@ export default function compose(state = initialState, action) { | |||
|   case COMPOSE_UPLOAD_CHANGE_SUCCESS: | ||||
|     return state | ||||
|       .set('is_changing_upload', false) | ||||
|       .setIn(['media_modal', 'dirty'], false) | ||||
|       .update('media_attachments', list => list.map(item => { | ||||
|         if (item.get('id') === action.media.id) { | ||||
|           return fromJS(action.media); | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; | ||||
| import { TIMELINE_DELETE } from '../actions/timelines'; | ||||
| import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; | ||||
| import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable'; | ||||
| 
 | ||||
| export default function modal(state = ImmutableStack(), action) { | ||||
|  | @ -8,6 +9,8 @@ export default function modal(state = ImmutableStack(), action) { | |||
|     return state.unshift(ImmutableMap({ modalType: action.modalType, modalProps: action.modalProps })); | ||||
|   case MODAL_CLOSE: | ||||
|     return (action.modalType === undefined || action.modalType === state.getIn([0, 'modalType'])) ? state.shift() : state; | ||||
|   case COMPOSE_UPLOAD_CHANGE_SUCCESS: | ||||
|     return state.getIn([0, 'modalType']) === 'FOCAL_POINT' ? state.shift() : state; | ||||
|   case TIMELINE_DELETE: | ||||
|     return state.filterNot((modal) => modal.get('modalProps').statusId === action.id); | ||||
|   default: | ||||
|  |  | |||
|  | @ -223,8 +223,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity | |||
|     emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri) | ||||
|     emoji.image_remote_url = image_url | ||||
|     emoji.save | ||||
|   rescue Seahorse::Client::NetworkingError | ||||
|     nil | ||||
|   rescue Seahorse::Client::NetworkingError => e | ||||
|     Rails.logger.warn "Error storing emoji: #{e}" | ||||
|   end | ||||
| 
 | ||||
|   def process_attachments | ||||
|  | @ -247,8 +247,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity | |||
|         media_attachment.save | ||||
|       rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError | ||||
|         RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) | ||||
|       rescue Seahorse::Client::NetworkingError | ||||
|         nil | ||||
|       rescue Seahorse::Client::NetworkingError => e | ||||
|         Rails.logger.warn "Error storing media attachment: #{e}" | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -168,7 +168,7 @@ class BackupService < BaseService | |||
|         io.write(buffer) | ||||
|       end | ||||
|     end | ||||
|   rescue Errno::ENOENT, Seahorse::Client::NetworkingError | ||||
|     Rails.logger.warn "Could not backup file #{filename}: file not found" | ||||
|   rescue Errno::ENOENT, Seahorse::Client::NetworkingError => e | ||||
|     Rails.logger.warn "Could not backup file #{filename}: #{e}" | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ class RemoveStatusService < BaseService | |||
|     # because once original status is gone, reblogs will disappear | ||||
|     # without us being able to do all the fancy stuff | ||||
| 
 | ||||
|     @status.reblogs.includes(:account).find_each do |reblog| | ||||
|     @status.reblogs.includes(:account).reorder(nil).find_each do |reblog| | ||||
|       RemoveStatusService.new.call(reblog, original_removed: true) | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ | |||
|     "color-blend": "^3.0.1", | ||||
|     "compression-webpack-plugin": "^6.1.1", | ||||
|     "cross-env": "^7.0.3", | ||||
|     "css-loader": "^5.2.6", | ||||
|     "css-loader": "^5.2.7", | ||||
|     "cssnano": "^4.1.11", | ||||
|     "detect-passive-events": "^2.0.3", | ||||
|     "dotenv": "^9.0.2", | ||||
|  | @ -171,14 +171,14 @@ | |||
|     "webpack-cli": "^3.3.12", | ||||
|     "webpack-merge": "^5.8.0", | ||||
|     "wicg-inert": "^3.1.1", | ||||
|     "ws": "^7.5.2" | ||||
|     "ws": "^7.5.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@testing-library/jest-dom": "^5.14.1", | ||||
|     "@testing-library/react": "^11.2.7", | ||||
|     "babel-eslint": "^10.1.0", | ||||
|     "babel-jest": "^27.0.6", | ||||
|     "eslint": "^7.30.0", | ||||
|     "eslint": "^7.31.0", | ||||
|     "eslint-plugin-import": "~2.23.4", | ||||
|     "eslint-plugin-jsx-a11y": "~6.4.1", | ||||
|     "eslint-plugin-promise": "~5.1.0", | ||||
|  |  | |||
							
								
								
									
										34
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								yarn.lock
									
									
									
									
									
								
							|  | @ -1092,10 +1092,10 @@ | |||
|   resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" | ||||
|   integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== | ||||
| 
 | ||||
| "@eslint/eslintrc@^0.4.2": | ||||
|   version "0.4.2" | ||||
|   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179" | ||||
|   integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg== | ||||
| "@eslint/eslintrc@^0.4.3": | ||||
|   version "0.4.3" | ||||
|   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" | ||||
|   integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== | ||||
|   dependencies: | ||||
|     ajv "^6.12.4" | ||||
|     debug "^4.1.1" | ||||
|  | @ -3465,10 +3465,10 @@ css-list-helpers@^1.0.1: | |||
|   dependencies: | ||||
|     tcomb "^2.5.0" | ||||
| 
 | ||||
| css-loader@^5.2.6: | ||||
|   version "5.2.6" | ||||
|   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.6.tgz#c3c82ab77fea1f360e587d871a6811f4450cc8d1" | ||||
|   integrity sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w== | ||||
| css-loader@^5.2.7: | ||||
|   version "5.2.7" | ||||
|   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae" | ||||
|   integrity sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg== | ||||
|   dependencies: | ||||
|     icss-utils "^5.1.0" | ||||
|     loader-utils "^2.0.0" | ||||
|  | @ -4455,13 +4455,13 @@ eslint@^2.7.0: | |||
|     text-table "~0.2.0" | ||||
|     user-home "^2.0.0" | ||||
| 
 | ||||
| eslint@^7.30.0: | ||||
|   version "7.30.0" | ||||
|   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.30.0.tgz#6d34ab51aaa56112fd97166226c9a97f505474f8" | ||||
|   integrity sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg== | ||||
| eslint@^7.31.0: | ||||
|   version "7.31.0" | ||||
|   resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca" | ||||
|   integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA== | ||||
|   dependencies: | ||||
|     "@babel/code-frame" "7.12.11" | ||||
|     "@eslint/eslintrc" "^0.4.2" | ||||
|     "@eslint/eslintrc" "^0.4.3" | ||||
|     "@humanwhocodes/config-array" "^0.5.0" | ||||
|     ajv "^6.10.0" | ||||
|     chalk "^4.0.0" | ||||
|  | @ -11730,10 +11730,10 @@ ws@^6.2.1: | |||
|   dependencies: | ||||
|     async-limiter "~1.0.0" | ||||
| 
 | ||||
| ws@^7.2.3, ws@^7.3.1, ws@^7.5.2: | ||||
|   version "7.5.2" | ||||
|   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.2.tgz#09cc8fea3bec1bc5ed44ef51b42f945be36900f6" | ||||
|   integrity sha512-lkF7AWRicoB9mAgjeKbGqVUekLnSNO4VjKVnuPHpQeOxZOErX6BPXwJk70nFslRCEEA8EVW7ZjKwXaP9N+1sKQ== | ||||
| ws@^7.2.3, ws@^7.3.1, ws@^7.5.3: | ||||
|   version "7.5.3" | ||||
|   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" | ||||
|   integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== | ||||
| 
 | ||||
| xml-name-validator@^3.0.0: | ||||
|   version "3.0.0" | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue