From 3a27967846bb84887bab41ac275dfe9158fffd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 5 Apr 2024 14:00:07 +0200 Subject: [PATCH] Add ability to reorder uploaded media before posting in web UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on Gargron's changes in Mastodon: https://github.com/mastodon/mastodon/pull/28456 Signed-off-by: marcin mikołajczak --- src/actions/compose.ts | 12 +++++++ src/components/upload.tsx | 18 ++++++++-- .../compose/components/upload-form.tsx | 34 +++++++++++++++++-- src/features/compose/components/upload.tsx | 18 ++++++++-- src/reducers/compose.ts | 9 +++++ 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/actions/compose.ts b/src/actions/compose.ts index ed51f2a0c..99f84298e 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -89,6 +89,8 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const; const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const; +const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const; + const messages = defineMessages({ scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' }, @@ -851,6 +853,13 @@ const setEditorState = (composeId: string, editorState: EditorState | string | n editorState: editorState, }); +const changeMediaOrder = (composeId: string, a: string, b: string) => ({ + type: COMPOSE_CHANGE_MEDIA_ORDER, + id: composeId, + a, + b, +}); + type ComposeAction = ComposeSetStatusAction | ReturnType @@ -897,6 +906,7 @@ type ComposeAction = | ComposeRemoveFromMentionsAction | ComposeEventReplyAction | ReturnType + | ReturnType export { COMPOSE_CHANGE, @@ -945,6 +955,7 @@ export { COMPOSE_SET_STATUS, COMPOSE_EDITOR_STATE_SET, COMPOSE_SET_GROUP_TIMELINE_VISIBLE, + COMPOSE_CHANGE_MEDIA_ORDER, setComposeToStatus, changeCompose, replyCompose, @@ -1000,5 +1011,6 @@ export { removeFromMentions, eventDiscussionCompose, setEditorState, + changeMediaOrder, type ComposeAction, }; diff --git a/src/components/upload.tsx b/src/components/upload.tsx index e83dd4d49..b5f29b38f 100644 --- a/src/components/upload.tsx +++ b/src/components/upload.tsx @@ -61,7 +61,7 @@ const messages = defineMessages({ descriptionMissingTitle: { id: 'upload_form.description_missing.title', defaultMessage: 'This attachment doesn\'t have a description' }, }); -interface IUpload { +interface IUpload extends Pick, 'onDragStart' | 'onDragEnter' | 'onDragEnd'> { media: Attachment; onSubmit?(): void; onDelete?(): void; @@ -75,6 +75,9 @@ const Upload: React.FC = ({ onSubmit, onDelete, onDescriptionChange, + onDragStart, + onDragEnter, + onDragEnd, descriptionLimit, withPreview = true, }) => { @@ -151,7 +154,18 @@ const Upload: React.FC = ({ ); return ( -
+
{({ scale }) => ( diff --git a/src/features/compose/components/upload-form.tsx b/src/features/compose/components/upload-form.tsx index c086ae359..ac6ea3d8d 100644 --- a/src/features/compose/components/upload-form.tsx +++ b/src/features/compose/components/upload-form.tsx @@ -1,8 +1,9 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { useCallback, useRef } from 'react'; +import { changeMediaOrder } from 'soapbox/actions/compose'; import { HStack } from 'soapbox/components/ui'; -import { useCompose } from 'soapbox/hooks'; +import { useAppDispatch, useCompose } from 'soapbox/hooks'; import Upload from './upload'; import UploadProgress from './upload-progress'; @@ -15,15 +16,42 @@ interface IUploadForm { } const UploadForm: React.FC = ({ composeId, onSubmit }) => { + const dispatch = useAppDispatch(); + const mediaIds = useCompose(composeId).media_attachments.map((item: AttachmentEntity) => item.id); + const dragItem = useRef(); + const dragOverItem = useRef(); + + const handleDragStart = useCallback((id: string) => { + dragItem.current = id; + }, [dragItem]); + + const handleDragEnter = useCallback((id: string) => { + dragOverItem.current = id; + }, [dragOverItem]); + + const handleDragEnd = useCallback(() => { + dispatch(changeMediaOrder(composeId, dragItem.current!, dragOverItem.current!)); + dragItem.current = null; + dragOverItem.current = null; + }, [dragItem, dragOverItem]); + return (
{mediaIds.map((id: string) => ( - + ))}
diff --git a/src/features/compose/components/upload.tsx b/src/features/compose/components/upload.tsx index a7f89d487..5ba440996 100644 --- a/src/features/compose/components/upload.tsx +++ b/src/features/compose/components/upload.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { undoUploadCompose, changeUploadCompose } from 'soapbox/actions/compose'; import Upload from 'soapbox/components/upload'; @@ -8,9 +8,12 @@ interface IUploadCompose { id: string; composeId: string; onSubmit?(): void; + onDragStart: (id: string) => void; + onDragEnter: (id: string) => void; + onDragEnd: () => void; } -const UploadCompose: React.FC = ({ composeId, id, onSubmit }) => { +const UploadCompose: React.FC = ({ composeId, id, onSubmit, onDragStart, onDragEnter, onDragEnd }) => { const dispatch = useAppDispatch(); const { pleroma: { metadata: { description_limit: descriptionLimit } } } = useInstance(); @@ -24,12 +27,23 @@ const UploadCompose: React.FC = ({ composeId, id, onSubmit }) => dispatch(undoUploadCompose(composeId, media.id)); }; + const handleDragStart = useCallback(() => { + onDragStart(id); + }, [onDragStart, id]); + + const handleDragEnter = useCallback(() => { + onDragEnter(id); + }, [onDragEnter, id]); + return ( diff --git a/src/reducers/compose.ts b/src/reducers/compose.ts index b9f064998..624ea0b3a 100644 --- a/src/reducers/compose.ts +++ b/src/reducers/compose.ts @@ -54,6 +54,7 @@ import { COMPOSE_EDITOR_STATE_SET, COMPOSE_SET_GROUP_TIMELINE_VISIBLE, ComposeAction, + COMPOSE_CHANGE_MEDIA_ORDER, } from '../actions/compose'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; @@ -520,6 +521,14 @@ export default function compose(state = initialState, action: ComposeAction | Ev return updateCompose(state, 'event-compose-modal', compose => compose.set('text', '')); case EVENT_FORM_SET: return updateCompose(state, 'event-compose-modal', compose => compose.set('text', action.text)); + case COMPOSE_CHANGE_MEDIA_ORDER: + return updateCompose(state, action.id, compose => compose.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; }