Merge branch 'feature-media-reordering' into 'main'

Add ability to reorder uploaded media before posting in web UI

See merge request soapbox-pub/soapbox!2984
This commit is contained in:
marcin mikołajczak 2024-04-05 13:49:18 +00:00
commit 93e9760274
5 changed files with 84 additions and 7 deletions

View File

@ -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_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const;
const messages = defineMessages({ const messages = defineMessages({
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, 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!' }, success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' },
@ -851,6 +853,13 @@ const setEditorState = (composeId: string, editorState: EditorState | string | n
editorState: editorState, editorState: editorState,
}); });
const changeMediaOrder = (composeId: string, a: string, b: string) => ({
type: COMPOSE_CHANGE_MEDIA_ORDER,
id: composeId,
a,
b,
});
type ComposeAction = type ComposeAction =
ComposeSetStatusAction ComposeSetStatusAction
| ReturnType<typeof changeCompose> | ReturnType<typeof changeCompose>
@ -897,6 +906,7 @@ type ComposeAction =
| ComposeRemoveFromMentionsAction | ComposeRemoveFromMentionsAction
| ComposeEventReplyAction | ComposeEventReplyAction
| ReturnType<typeof setEditorState> | ReturnType<typeof setEditorState>
| ReturnType<typeof changeMediaOrder>
export { export {
COMPOSE_CHANGE, COMPOSE_CHANGE,
@ -945,6 +955,7 @@ export {
COMPOSE_SET_STATUS, COMPOSE_SET_STATUS,
COMPOSE_EDITOR_STATE_SET, COMPOSE_EDITOR_STATE_SET,
COMPOSE_SET_GROUP_TIMELINE_VISIBLE, COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
COMPOSE_CHANGE_MEDIA_ORDER,
setComposeToStatus, setComposeToStatus,
changeCompose, changeCompose,
replyCompose, replyCompose,
@ -1000,5 +1011,6 @@ export {
removeFromMentions, removeFromMentions,
eventDiscussionCompose, eventDiscussionCompose,
setEditorState, setEditorState,
changeMediaOrder,
type ComposeAction, type ComposeAction,
}; };

View File

@ -61,7 +61,7 @@ const messages = defineMessages({
descriptionMissingTitle: { id: 'upload_form.description_missing.title', defaultMessage: 'This attachment doesn\'t have a description' }, descriptionMissingTitle: { id: 'upload_form.description_missing.title', defaultMessage: 'This attachment doesn\'t have a description' },
}); });
interface IUpload { interface IUpload extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onDragStart' | 'onDragEnter' | 'onDragEnd'> {
media: Attachment; media: Attachment;
onSubmit?(): void; onSubmit?(): void;
onDelete?(): void; onDelete?(): void;
@ -75,6 +75,9 @@ const Upload: React.FC<IUpload> = ({
onSubmit, onSubmit,
onDelete, onDelete,
onDescriptionChange, onDescriptionChange,
onDragStart,
onDragEnter,
onDragEnd,
descriptionLimit, descriptionLimit,
withPreview = true, withPreview = true,
}) => { }) => {
@ -151,7 +154,18 @@ const Upload: React.FC<IUpload> = ({
); );
return ( return (
<div className='compose-form__upload' tabIndex={0} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleClick} role='button'> <div
className='compose-form__upload'
tabIndex={0}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
role='button'
draggable
onDragStart={onDragStart}
onDragEnter={onDragEnter}
onDragEnd={onDragEnd}
>
<Blurhash hash={media.blurhash} className='media-gallery__preview' /> <Blurhash hash={media.blurhash} className='media-gallery__preview' />
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => ( {({ scale }) => (

View File

@ -1,8 +1,9 @@
import clsx from 'clsx'; 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 { HStack } from 'soapbox/components/ui';
import { useCompose } from 'soapbox/hooks'; import { useAppDispatch, useCompose } from 'soapbox/hooks';
import Upload from './upload'; import Upload from './upload';
import UploadProgress from './upload-progress'; import UploadProgress from './upload-progress';
@ -15,15 +16,42 @@ interface IUploadForm {
} }
const UploadForm: React.FC<IUploadForm> = ({ composeId, onSubmit }) => { const UploadForm: React.FC<IUploadForm> = ({ composeId, onSubmit }) => {
const dispatch = useAppDispatch();
const mediaIds = useCompose(composeId).media_attachments.map((item: AttachmentEntity) => item.id); const mediaIds = useCompose(composeId).media_attachments.map((item: AttachmentEntity) => item.id);
const dragItem = useRef<string | null>();
const dragOverItem = useRef<string | null>();
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 ( return (
<div className='overflow-hidden'> <div className='overflow-hidden'>
<UploadProgress composeId={composeId} /> <UploadProgress composeId={composeId} />
<HStack wrap className={clsx('overflow-hidden', mediaIds.size !== 0 && 'p-1')}> <HStack wrap className={clsx('overflow-hidden', mediaIds.size !== 0 && 'p-1')}>
{mediaIds.map((id: string) => ( {mediaIds.map((id: string) => (
<Upload id={id} key={id} composeId={composeId} onSubmit={onSubmit} /> <Upload
id={id}
key={id}
composeId={composeId}
onSubmit={onSubmit}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragEnd={handleDragEnd}
/>
))} ))}
</HStack> </HStack>
</div> </div>

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useCallback } from 'react';
import { undoUploadCompose, changeUploadCompose } from 'soapbox/actions/compose'; import { undoUploadCompose, changeUploadCompose } from 'soapbox/actions/compose';
import Upload from 'soapbox/components/upload'; import Upload from 'soapbox/components/upload';
@ -8,9 +8,12 @@ interface IUploadCompose {
id: string; id: string;
composeId: string; composeId: string;
onSubmit?(): void; onSubmit?(): void;
onDragStart: (id: string) => void;
onDragEnter: (id: string) => void;
onDragEnd: () => void;
} }
const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit }) => { const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit, onDragStart, onDragEnter, onDragEnd }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { pleroma: { metadata: { description_limit: descriptionLimit } } } = useInstance(); const { pleroma: { metadata: { description_limit: descriptionLimit } } } = useInstance();
@ -24,12 +27,23 @@ const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id, onSubmit }) =>
dispatch(undoUploadCompose(composeId, media.id)); dispatch(undoUploadCompose(composeId, media.id));
}; };
const handleDragStart = useCallback(() => {
onDragStart(id);
}, [onDragStart, id]);
const handleDragEnter = useCallback(() => {
onDragEnter(id);
}, [onDragEnter, id]);
return ( return (
<Upload <Upload
media={media} media={media}
onDelete={handleDelete} onDelete={handleDelete}
onDescriptionChange={handleDescriptionChange} onDescriptionChange={handleDescriptionChange}
onSubmit={onSubmit} onSubmit={onSubmit}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragEnd={onDragEnd}
descriptionLimit={descriptionLimit} descriptionLimit={descriptionLimit}
withPreview withPreview
/> />

View File

@ -54,6 +54,7 @@ import {
COMPOSE_EDITOR_STATE_SET, COMPOSE_EDITOR_STATE_SET,
COMPOSE_SET_GROUP_TIMELINE_VISIBLE, COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
ComposeAction, ComposeAction,
COMPOSE_CHANGE_MEDIA_ORDER,
} from '../actions/compose'; } from '../actions/compose';
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; 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', '')); return updateCompose(state, 'event-compose-modal', compose => compose.set('text', ''));
case EVENT_FORM_SET: case EVENT_FORM_SET:
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', action.text)); 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: default:
return state; return state;
} }