From 8e7e86ee354fca36b989b6de40904f38e6db103b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 25 Mar 2024 11:29:55 +0100 Subject: [PATCH] Add ability to reorder uploaded media before posting in web UI (#28456) --- app/javascript/mastodon/actions/compose.js | 7 ++ .../compose/components/compose_form.jsx | 4 +- .../features/compose/components/upload.jsx | 114 +++++++++--------- .../compose/components/upload_form.jsx | 70 +++++++---- .../compose/components/upload_progress.jsx | 77 ++++++------ .../compose/containers/upload_container.js | 27 ----- .../containers/upload_form_container.js | 9 -- .../containers/upload_progress_container.js | 11 -- .../ui/components/focal_point_modal.jsx | 2 +- app/javascript/mastodon/reducers/compose.js | 9 ++ 10 files changed, 159 insertions(+), 171 deletions(-) delete mode 100644 app/javascript/mastodon/features/compose/containers/upload_container.js delete mode 100644 app/javascript/mastodon/features/compose/containers/upload_form_container.js delete mode 100644 app/javascript/mastodon/features/compose/containers/upload_progress_container.js 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; }