diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 12bd43f807..7477e45e5e 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -75,6 +75,7 @@ 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';
+export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
@@ -809,3 +810,9 @@ export function changePollSettings(expiresIn, isMultiple) {
isMultiple,
};
}
+
+export const changeMediaOrder = (a, b) => ({
+ type: COMPOSE_CHANGE_MEDIA_ORDER,
+ a,
+ b,
+});
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx
index b93bac9d19..9b4d3dfeb5 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx
@@ -21,7 +21,6 @@ import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
-import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container';
import { countableText } from '../util/counter';
@@ -30,6 +29,7 @@ import { EditIndicator } from './edit_indicator';
import { NavigationBar } from './navigation_bar';
import { PollForm } from "./poll_form";
import { ReplyIndicator } from './reply_indicator';
+import { UploadForm } from './upload_form';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@@ -283,7 +283,7 @@ class ComposeForm extends ImmutablePureComponent {
/>
-
+
diff --git a/app/javascript/mastodon/features/compose/components/upload.jsx b/app/javascript/mastodon/features/compose/components/upload.jsx
index e8045ae81f..7f6ef6cfd8 100644
--- a/app/javascript/mastodon/features/compose/components/upload.jsx
+++ b/app/javascript/mastodon/features/compose/components/upload.jsx
@@ -1,77 +1,81 @@
import PropTypes from 'prop-types';
+import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import { useDispatch, useSelector } from 'react-redux';
import spring from 'react-motion/lib/spring';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
+import { undoUploadCompose, initMediaEditModal } from 'mastodon/actions/compose';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
+import Motion from 'mastodon/features/ui/util/optional_motion';
-import Motion from '../../ui/util/optional_motion';
+export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
+ const dispatch = useDispatch();
+ const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
+ const sensitive = useSelector(state => state.getIn(['compose', 'spoiler']));
-export default class Upload extends ImmutablePureComponent {
+ const handleUndoClick = useCallback(() => {
+ dispatch(undoUploadCompose(id));
+ }, [dispatch, id]);
- static propTypes = {
- media: ImmutablePropTypes.map.isRequired,
- sensitive: PropTypes.bool,
- onUndo: PropTypes.func.isRequired,
- onOpenFocalPoint: PropTypes.func.isRequired,
- };
+ const handleFocalPointClick = useCallback(() => {
+ dispatch(initMediaEditModal(id));
+ }, [dispatch, id]);
- handleUndoClick = e => {
- e.stopPropagation();
- this.props.onUndo(this.props.media.get('id'));
- };
+ const handleDragStart = useCallback(() => {
+ onDragStart(id);
+ }, [onDragStart, id]);
- handleFocalPointClick = e => {
- e.stopPropagation();
- this.props.onOpenFocalPoint(this.props.media.get('id'));
- };
+ const handleDragEnter = useCallback(() => {
+ onDragEnter(id);
+ }, [onDragEnter, id]);
- render () {
- const { media, sensitive } = this.props;
-
- if (!media) {
- return null;
- }
-
- const focusX = media.getIn(['meta', 'focus', 'x']);
- const focusY = media.getIn(['meta', 'focus', 'y']);
- const x = ((focusX / 2) + .5) * 100;
- const y = ((focusY / -2) + .5) * 100;
- const missingDescription = (media.get('description') || '').length === 0;
-
- return (
-
-
- {({ scale }) => (
-
- {sensitive &&
}
-
-
-
-
-
-
-
-
-
-
- )}
-
-
- );
+ if (!media) {
+ return null;
}
-}
+ const focusX = media.getIn(['meta', 'focus', 'x']);
+ const focusY = media.getIn(['meta', 'focus', 'y']);
+ const x = ((focusX / 2) + .5) * 100;
+ const y = ((focusY / -2) + .5) * 100;
+ const missingDescription = (media.get('description') || '').length === 0;
+
+ return (
+
+
+ {({ scale }) => (
+
+ {sensitive &&
}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+Upload.propTypes = {
+ id: PropTypes.string,
+ onDragEnter: PropTypes.func,
+ onDragStart: PropTypes.func,
+ onDragEnd: PropTypes.func,
+};
diff --git a/app/javascript/mastodon/features/compose/components/upload_form.jsx b/app/javascript/mastodon/features/compose/components/upload_form.jsx
index 46bac7823b..adf5591382 100644
--- a/app/javascript/mastodon/features/compose/components/upload_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/upload_form.jsx
@@ -1,31 +1,53 @@
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import { useRef, useCallback } from 'react';
-import UploadContainer from '../containers/upload_container';
-import UploadProgressContainer from '../containers/upload_progress_container';
+import { useSelector, useDispatch } from 'react-redux';
-export default class UploadForm extends ImmutablePureComponent {
+import { changeMediaOrder } from 'mastodon/actions/compose';
- static propTypes = {
- mediaIds: ImmutablePropTypes.list.isRequired,
- };
+import { Upload } from './upload';
+import { UploadProgress } from './upload_progress';
- render () {
- const { mediaIds } = this.props;
+export const UploadForm = () => {
+ const dispatch = useDispatch();
+ const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
+ const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
+ const progress = useSelector(state => state.getIn(['compose', 'progress']));
+ const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
- return (
- <>
-
+ const dragItem = useRef();
+ const dragOverItem = useRef();
- {mediaIds.size > 0 && (
-
- {mediaIds.map(id => (
-
- ))}
-
- )}
- >
- );
- }
+ const handleDragStart = useCallback(id => {
+ dragItem.current = id;
+ }, [dragItem]);
-}
+ const handleDragEnter = useCallback(id => {
+ dragOverItem.current = id;
+ }, [dragOverItem]);
+
+ const handleDragEnd = useCallback(() => {
+ dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
+ dragItem.current = null;
+ dragOverItem.current = null;
+ }, [dispatch, dragItem, dragOverItem]);
+
+ return (
+ <>
+
+
+ {mediaIds.size > 0 && (
+
+ {mediaIds.map(id => (
+
+ ))}
+
+ )}
+ >
+ );
+};
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.jsx b/app/javascript/mastodon/features/compose/components/upload_progress.jsx
index 1276cded1f..fd0c8f4530 100644
--- a/app/javascript/mastodon/features/compose/components/upload_progress.jsx
+++ b/app/javascript/mastodon/features/compose/components/upload_progress.jsx
@@ -1,5 +1,4 @@
import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -10,46 +9,40 @@ import { Icon } from 'mastodon/components/icon';
import Motion from '../../ui/util/optional_motion';
-export default class UploadProgress extends PureComponent {
-
- static propTypes = {
- active: PropTypes.bool,
- progress: PropTypes.number,
- isProcessing: PropTypes.bool,
- };
-
- render () {
- const { active, progress, isProcessing } = this.props;
-
- if (!active) {
- return null;
- }
-
- let message;
-
- if (isProcessing) {
- message =
;
- } else {
- message =
;
- }
-
- return (
-
-
-
-
- {message}
-
-
-
- {({ width }) =>
-
- }
-
-
-
-
- );
+export const UploadProgress = ({ active, progress, isProcessing }) => {
+ if (!active) {
+ return null;
}
-}
+ let message;
+
+ if (isProcessing) {
+ message =
;
+ } else {
+ message =
;
+ }
+
+ return (
+
+
+
+
+ {message}
+
+
+
+ {({ width }) =>
+
+ }
+
+
+
+
+ );
+};
+
+UploadProgress.propTypes = {
+ active: PropTypes.bool,
+ progress: PropTypes.number,
+ isProcessing: PropTypes.bool,
+};
diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js
deleted file mode 100644
index a17a691444..0000000000
--- a/app/javascript/mastodon/features/compose/containers/upload_container.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { connect } from 'react-redux';
-
-import { undoUploadCompose, initMediaEditModal, submitCompose } from '../../../actions/compose';
-import Upload from '../components/upload';
-
-const mapStateToProps = (state, { id }) => ({
- media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
- sensitive: state.getIn(['compose', 'spoiler']),
-});
-
-const mapDispatchToProps = dispatch => ({
-
- onUndo: id => {
- dispatch(undoUploadCompose(id));
- },
-
- onOpenFocalPoint: id => {
- dispatch(initMediaEditModal(id));
- },
-
- onSubmit (router) {
- dispatch(submitCompose(router));
- },
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(Upload);
diff --git a/app/javascript/mastodon/features/compose/containers/upload_form_container.js b/app/javascript/mastodon/features/compose/containers/upload_form_container.js
deleted file mode 100644
index 336525cf53..0000000000
--- a/app/javascript/mastodon/features/compose/containers/upload_form_container.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { connect } from 'react-redux';
-
-import UploadForm from '../components/upload_form';
-
-const mapStateToProps = state => ({
- mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
-});
-
-export default connect(mapStateToProps)(UploadForm);
diff --git a/app/javascript/mastodon/features/compose/containers/upload_progress_container.js b/app/javascript/mastodon/features/compose/containers/upload_progress_container.js
deleted file mode 100644
index ffff321c3f..0000000000
--- a/app/javascript/mastodon/features/compose/containers/upload_progress_container.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { connect } from 'react-redux';
-
-import UploadProgress from '../components/upload_progress';
-
-const mapStateToProps = state => ({
- active: state.getIn(['compose', 'is_uploading']),
- progress: state.getIn(['compose', 'progress']),
- isProcessing: state.getIn(['compose', 'is_processing']),
-});
-
-export default connect(mapStateToProps)(UploadProgress);
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
index 5f430d5392..7adfc208e7 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
@@ -22,7 +22,7 @@ import { GIFV } from 'mastodon/components/gifv';
import { IconButton } from 'mastodon/components/icon_button';
import Audio from 'mastodon/features/audio';
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
-import UploadProgress from 'mastodon/features/compose/components/upload_progress';
+import { UploadProgress } from 'mastodon/features/compose/components/upload_progress';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import { me } from 'mastodon/initial_state';
import { assetHost } from 'mastodon/utils/config';
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 8dc2801857..97218e9f75 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -45,6 +45,7 @@ import {
INIT_MEDIA_EDIT_MODAL,
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
COMPOSE_CHANGE_MEDIA_FOCUS,
+ COMPOSE_CHANGE_MEDIA_ORDER,
COMPOSE_SET_STATUS,
COMPOSE_FOCUS,
} from '../actions/compose';
@@ -536,6 +537,14 @@ export default function compose(state = initialState, action) {
return state.set('language', action.language);
case COMPOSE_FOCUS:
return state.set('focusDate', new Date()).update('text', text => text.length > 0 ? text : action.defaultText);
+ case COMPOSE_CHANGE_MEDIA_ORDER:
+ return state.update('media_attachments', list => {
+ const indexA = list.findIndex(x => x.get('id') === action.a);
+ const moveItem = list.get(indexA);
+ const indexB = list.findIndex(x => x.get('id') === action.b);
+
+ return list.splice(indexA, 1).splice(indexB, 0, moveItem);
+ });
default:
return state;
}