From e80971e660945b08a4f34ab83ba97fcf2dfcfcdb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 27 Sep 2024 17:09:39 +0200 Subject: [PATCH] [Glitch] Change media reordering design in the compose form in web UI Port 11a12e56b3480ac03921e831a7ccfcc7ade24b52 to glitch-soc Signed-off-by: Claire --- .../compose/components/sensitive_button.jsx | 10 +- .../features/compose/components/upload.jsx | 81 -------- .../features/compose/components/upload.tsx | 130 ++++++++++++ .../compose/components/upload_form.jsx | 56 ------ .../compose/components/upload_form.tsx | 188 ++++++++++++++++++ .../glitch/models/media_attachment.ts | 2 + .../flavours/glitch/styles/components.scss | 59 ++++-- 7 files changed, 364 insertions(+), 162 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/compose/components/upload.jsx create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload.tsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_form.jsx create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_form.tsx create mode 100644 app/javascript/flavours/glitch/models/media_attachment.ts diff --git a/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx b/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx index deb401bfb7..de7d033ed0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx @@ -21,11 +21,11 @@ const messages = defineMessages({ export const SensitiveButton = () => { const intl = useIntl(); - const spoilersAlwaysOn = useAppSelector((state) => state.getIn(['local_settings', 'always_show_spoilers_field'])); - const spoilerText = useAppSelector((state) => state.getIn(['compose', 'spoiler_text'])); - const sensitive = useAppSelector((state) => state.getIn(['compose', 'sensitive'])); - const spoiler = useAppSelector((state) => state.getIn(['compose', 'spoiler'])); - const mediaCount = useAppSelector((state) => state.getIn(['compose', 'media_attachments']).size); + const spoilersAlwaysOn = useAppSelector((state) => state.local_settings.getIn(['always_show_spoilers_field'])); + const spoilerText = useAppSelector((state) => state.compose.get('spoiler_text')); + const sensitive = useAppSelector((state) => state.compose.get('sensitive')); + const spoiler = useAppSelector((state) => state.compose.get('spoiler')); + const mediaCount = useAppSelector((state) => state.compose.get('media_attachments').size); const disabled = spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler; const active = sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0); diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.jsx b/app/javascript/flavours/glitch/features/compose/components/upload.jsx deleted file mode 100644 index 790d76264d..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/upload.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import PropTypes from 'prop-types'; -import { useCallback } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - -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 'flavours/glitch/actions/compose'; -import { Blurhash } from 'flavours/glitch/components/blurhash'; -import { Icon } from 'flavours/glitch/components/icon'; -import Motion from 'flavours/glitch/features/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', 'sensitive'])); - - const handleUndoClick = useCallback(() => { - dispatch(undoUploadCompose(id)); - }, [dispatch, id]); - - const handleFocalPointClick = useCallback(() => { - dispatch(initMediaEditModal(id)); - }, [dispatch, id]); - - const handleDragStart = useCallback(() => { - onDragStart(id); - }, [onDragStart, id]); - - const handleDragEnter = useCallback(() => { - onDragEnter(id); - }, [onDragEnter, id]); - - 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/flavours/glitch/features/compose/components/upload.tsx b/app/javascript/flavours/glitch/features/compose/components/upload.tsx new file mode 100644 index 0000000000..a583a8d81d --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload.tsx @@ -0,0 +1,130 @@ +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + +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 'flavours/glitch/actions/compose'; +import { Blurhash } from 'flavours/glitch/components/blurhash'; +import { Icon } from 'flavours/glitch/components/icon'; +import type { MediaAttachment } from 'flavours/glitch/models/media_attachment'; +import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; + +export const Upload: React.FC<{ + id: string; + dragging?: boolean; + overlay?: boolean; + tall?: boolean; + wide?: boolean; +}> = ({ id, dragging, overlay, tall, wide }) => { + const dispatch = useAppDispatch(); + const media = useAppSelector( + (state) => + state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call + .get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access + .find((item: MediaAttachment) => item.get('id') === id) as // eslint-disable-line @typescript-eslint/no-unsafe-member-access + | MediaAttachment + | undefined, + ); + const sensitive = useAppSelector( + (state) => state.compose.get('sensitive') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + ); + + const handleUndoClick = useCallback(() => { + dispatch(undoUploadCompose(id)); + }, [dispatch, id]); + + const handleFocalPointClick = useCallback(() => { + dispatch(initMediaEditModal(id)); + }, [dispatch, id]); + + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id }); + + if (!media) { + return null; + } + + const focusX = media.getIn(['meta', 'focus', 'x']) as number; + const focusY = media.getIn(['meta', 'focus', 'y']) as number; + const x = (focusX / 2 + 0.5) * 100; + const y = (focusY / -2 + 0.5) * 100; + const missingDescription = + ((media.get('description') as string | undefined) ?? '').length === 0; + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+
+ {sensitive && ( + + )} + +
+ + +
+ +
+ +
+
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx deleted file mode 100644 index 2b26735f5e..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useRef, useCallback } from 'react'; - -import { useSelector, useDispatch } from 'react-redux'; - -import { changeMediaOrder } from 'flavours/glitch/actions/compose'; - -import { SensitiveButton } from './sensitive_button'; -import { Upload } from './upload'; -import { UploadProgress } from './upload_progress'; - -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'])); - - const dragItem = useRef(); - const dragOverItem = useRef(); - - 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 => ( - - ))} -
- )} - - {!mediaIds.isEmpty() && } - - ); -}; diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.tsx b/app/javascript/flavours/glitch/features/compose/components/upload_form.tsx new file mode 100644 index 0000000000..44bfaa8f38 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.tsx @@ -0,0 +1,188 @@ +import { useState, useCallback, useMemo } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import type { List } from 'immutable'; + +import type { + DragStartEvent, + DragEndEvent, + UniqueIdentifier, + Announcements, + ScreenReaderInstructions, +} from '@dnd-kit/core'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragOverlay, +} from '@dnd-kit/core'; +import { + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy, +} from '@dnd-kit/sortable'; + +import { changeMediaOrder } from 'flavours/glitch/actions/compose'; +import type { MediaAttachment } from 'flavours/glitch/models/media_attachment'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; + +import { SensitiveButton } from './sensitive_button'; +import { Upload } from './upload'; +import { UploadProgress } from './upload_progress'; + +const messages = defineMessages({ + screenReaderInstructions: { + id: 'upload_form.drag_and_drop.instructions', + defaultMessage: + 'To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.', + }, + onDragStart: { + id: 'upload_form.drag_and_drop.on_drag_start', + defaultMessage: 'Picked up media attachment {item}.', + }, + onDragOver: { + id: 'upload_form.drag_and_drop.on_drag_over', + defaultMessage: 'Media attachment {item} was moved.', + }, + onDragEnd: { + id: 'upload_form.drag_and_drop.on_drag_end', + defaultMessage: 'Media attachment {item} was dropped.', + }, + onDragCancel: { + id: 'upload_form.drag_and_drop.on_drag_cancel', + defaultMessage: + 'Dragging was cancelled. Media attachment {item} was dropped.', + }, +}); + +export const UploadForm: React.FC = () => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const mediaIds = useAppSelector( + (state) => + state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call + .get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access + .map((item: MediaAttachment) => item.get('id')) as List, // eslint-disable-line @typescript-eslint/no-unsafe-member-access + ); + const active = useAppSelector( + (state) => state.compose.get('is_uploading') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + ); + const progress = useAppSelector( + (state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + ); + const isProcessing = useAppSelector( + (state) => state.compose.get('is_processing') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + ); + const [activeId, setActiveId] = useState(null); + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragStart = useCallback( + (e: DragStartEvent) => { + const { active } = e; + + setActiveId(active.id); + }, + [setActiveId], + ); + + const handleDragEnd = useCallback( + (e: DragEndEvent) => { + const { active, over } = e; + + if (over && active.id !== over.id) { + dispatch(changeMediaOrder(active.id, over.id)); + } + + setActiveId(null); + }, + [dispatch, setActiveId], + ); + + const accessibility: { + screenReaderInstructions: ScreenReaderInstructions; + announcements: Announcements; + } = useMemo( + () => ({ + screenReaderInstructions: { + draggable: intl.formatMessage(messages.screenReaderInstructions), + }, + + announcements: { + onDragStart({ active }) { + return intl.formatMessage(messages.onDragStart, { item: active.id }); + }, + + onDragOver({ active }) { + return intl.formatMessage(messages.onDragOver, { item: active.id }); + }, + + onDragEnd({ active }) { + return intl.formatMessage(messages.onDragEnd, { item: active.id }); + }, + + onDragCancel({ active }) { + return intl.formatMessage(messages.onDragCancel, { item: active.id }); + }, + }, + }), + [intl], + ); + + return ( + <> + + + {mediaIds.size > 0 && ( +
+ + + {mediaIds.map((id, idx) => ( + + ))} + + + + {activeId ? : null} + + +
+ )} + + {!mediaIds.isEmpty() && } + + ); +}; diff --git a/app/javascript/flavours/glitch/models/media_attachment.ts b/app/javascript/flavours/glitch/models/media_attachment.ts new file mode 100644 index 0000000000..0e5b9ab555 --- /dev/null +++ b/app/javascript/flavours/glitch/models/media_attachment.ts @@ -0,0 +1,2 @@ +// Temporary until we type it correctly +export type MediaAttachment = Immutable.Map; diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 211edcc9be..72613cbbc4 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -653,19 +653,39 @@ body > [data-popper-placement] { } &__uploads { - display: flex; - gap: 8px; padding: 0 12px; - flex-wrap: wrap; - align-self: stretch; - align-items: flex-start; - align-content: flex-start; - justify-content: center; + aspect-ratio: 3/2; + } + + .media-gallery { + gap: 8px; } &__upload { - flex: 1 1 0; - min-width: calc(50% - 8px); + position: relative; + cursor: grab; + + &.dragging { + opacity: 0; + } + + &.overlay { + height: 100%; + border-radius: 8px; + pointer-events: none; + } + + &__drag-handle { + position: absolute; + top: 50%; + inset-inline-start: 0; + transform: translateY(-50%); + color: $white; + background: transparent; + border: 0; + padding: 8px 3px; + cursor: grab; + } &__actions { display: flex; @@ -686,8 +706,7 @@ body > [data-popper-placement] { &__thumbnail { width: 100%; - height: 144px; - border-radius: 6px; + height: 100%; background-position: center; background-size: cover; background-repeat: no-repeat; @@ -7602,30 +7621,30 @@ img.modal-warning { gap: 2px; &--layout-2 { - .media-gallery__item:nth-child(1) { + & > .media-gallery__item:nth-child(1) { border-end-end-radius: 0; border-start-end-radius: 0; } - .media-gallery__item:nth-child(2) { + & > .media-gallery__item:nth-child(2) { border-start-start-radius: 0; border-end-start-radius: 0; } } &--layout-3 { - .media-gallery__item:nth-child(1) { + & > .media-gallery__item:nth-child(1) { border-end-end-radius: 0; border-start-end-radius: 0; } - .media-gallery__item:nth-child(2) { + & > .media-gallery__item:nth-child(2) { border-start-start-radius: 0; border-end-start-radius: 0; border-end-end-radius: 0; } - .media-gallery__item:nth-child(3) { + & > .media-gallery__item:nth-child(3) { border-start-start-radius: 0; border-end-start-radius: 0; border-start-end-radius: 0; @@ -7633,26 +7652,26 @@ img.modal-warning { } &--layout-4 { - .media-gallery__item:nth-child(1) { + & > .media-gallery__item:nth-child(1) { border-end-end-radius: 0; border-start-end-radius: 0; border-end-start-radius: 0; } - .media-gallery__item:nth-child(2) { + & > .media-gallery__item:nth-child(2) { border-start-start-radius: 0; border-end-start-radius: 0; border-end-end-radius: 0; } - .media-gallery__item:nth-child(3) { + & > .media-gallery__item:nth-child(3) { border-start-start-radius: 0; border-start-end-radius: 0; border-end-start-radius: 0; border-end-end-radius: 0; } - .media-gallery__item:nth-child(4) { + & > .media-gallery__item:nth-child(4) { border-start-start-radius: 0; border-end-start-radius: 0; border-start-end-radius: 0;