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:
commit
93e9760274
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 }) => (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue