diff --git a/Gemfile b/Gemfile
index 5a7c1e1d3..467325822 100644
--- a/Gemfile
+++ b/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'
diff --git a/Gemfile.lock b/Gemfile.lock
index f17bcd87a..acde440de 100644
--- a/Gemfile.lock
+++ b/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)
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 85f4cc768..b863d8643 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9eb73d576..7c36bc6b8 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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?
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index a60373fd5..c844a907f 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -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());
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 295e83f58..ccc9067d1 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -309,8 +309,8 @@ class Status extends ImmutablePureComponent {
return (
- {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
- {status.get('content')}
+ {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
+ {status.get('content')}
);
diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js
index 342b0c2a9..05cd2ecc1 100644
--- a/app/javascript/mastodon/features/compose/containers/upload_container.js
+++ b/app/javascript/mastodon/features/compose/containers/upload_container.js
@@ -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) {
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 1ecb18bf8..f5ce50ac8 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -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')}`);
}
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index edeb281e9..a2e6b3d16 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -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}
/>
@@ -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 {
-
+
-