diff options
27 files changed, 344 insertions, 181 deletions
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/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f83738093..eebe98626 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -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()); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 782fd918e..d3944b0c3 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -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> ); diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js index f687fae99..f3ca4ce7b 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js @@ -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) { diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js index fcb2df527..98d1f40b2 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js @@ -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')}`); } diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index b7ec63333..5a4baa5a1 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/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 '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'> diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index 2636e79f5..62bb167a0 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -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> diff --git a/app/javascript/flavours/glitch/features/ui/containers/modal_container.js b/app/javascript/flavours/glitch/features/ui/containers/modal_container.js index e13e745e6..039aabd8a 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/modal_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/modal_container.js @@ -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 () { - dispatch(closeModal()); + onClose (confirmationMessage) { + if (confirmationMessage) { + dispatch( + openModal('CONFIRM', { + message: confirmationMessage.message, + confirm: confirmationMessage.confirm, + onConfirm: () => dispatch(closeModal()), + }), + ); + } else { + dispatch(closeModal()); + } }, }); diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index e989401d8..1735cfb4d 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -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); diff --git a/app/javascript/flavours/glitch/reducers/modal.js b/app/javascript/flavours/glitch/reducers/modal.js index f8fdc2995..ae205c6d5 100644 --- a/app/javascript/flavours/glitch/reducers/modal.js +++ b/app/javascript/flavours/glitch/reducers/modal.js @@ -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: 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 ( <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> ); 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} /> </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'> diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 3403830e4..377cccda5 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -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> diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js index ad1e8a2ee..34fec8206 100644 --- a/app/javascript/mastodon/features/ui/containers/modal_container.js +++ b/app/javascript/mastodon/features/ui/containers/modal_container.js @@ -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 () { - dispatch(closeModal()); + onClose (confirmationMessage) { + if (confirmationMessage) { + dispatch( + openModal('CONFIRM', { + message: confirmationMessage.message, + confirm: confirmationMessage.confirm, + onConfirm: () => dispatch(closeModal()), + }), + ); + } else { + dispatch(closeModal()); + } }, }); diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 4c0ba1c36..34c7c4dea 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -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); diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js index c43446f6d..41161a206 100644 --- a/app/javascript/mastodon/reducers/modal.js +++ b/app/javascript/mastodon/reducers/modal.js @@ -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: diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index cc2d391fb..390414df2 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -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 diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 749c84736..f07f407d8 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -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 diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 20c17e6df..9259c69d9 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -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 diff --git a/package.json b/package.json index 932911d6c..a4b13c418 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index ce0a12e63..d1532e4a6 100644 --- a/yarn.lock +++ b/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" |