From 5e7c0797875250a5f15ca8d7594c06e3aeeb139c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 21 Jan 2025 12:34:22 +0100 Subject: [PATCH] [Glitch] Change design of edit media modal in web UI Port 11786f1114f9dfb5a17810d14477a94162f94064 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/actions/compose.js | 2 +- .../flavours/glitch/actions/compose_typed.ts | 70 +++ .../flavours/glitch/actions/modal.ts | 1 + app/javascript/flavours/glitch/api/compose.ts | 7 + .../flavours/glitch/components/button.tsx | 3 + .../glitch/components/follow_button.tsx | 10 +- .../flavours/glitch/components/gifv.tsx | 98 ++-- .../glitch/features/alt_text_modal/index.tsx | 531 ++++++++++++++++++ .../flavours/glitch/features/audio/index.jsx | 12 +- .../compose/components/character_counter.jsx | 18 - .../compose/components/character_counter.tsx | 16 + .../compose/components/compose_form.jsx | 1 + .../features/compose/components/upload.tsx | 27 +- .../compose/components/upload_form.tsx | 20 +- .../components/notification_follow.tsx | 4 +- .../features/ui/components/boost_modal.tsx | 1 - .../ui/components/focal_point_modal.jsx | 438 --------------- .../features/ui/components/modal_root.jsx | 7 +- .../features/ui/containers/modal_container.js | 4 +- .../flavours/glitch/reducers/compose.js | 64 +-- .../flavours/glitch/reducers/index.ts | 4 +- .../flavours/glitch/reducers/modal.ts | 34 +- .../flavours/glitch/styles/components.scss | 232 ++------ .../flavours/glitch/styles/forms.scss | 29 +- .../glitch/styles/mastodon-light/diff.scss | 9 +- 25 files changed, 863 insertions(+), 779 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/compose_typed.ts create mode 100644 app/javascript/flavours/glitch/api/compose.ts create mode 100644 app/javascript/flavours/glitch/features/alt_text_modal/index.tsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/character_counter.jsx create mode 100644 app/javascript/flavours/glitch/features/compose/components/character_counter.tsx delete mode 100644 app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index e850a83a855a60..064a49f5c8eae0 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -448,7 +448,7 @@ export function initMediaEditModal(id) { dispatch(openModal({ modalType: 'FOCAL_POINT', - modalProps: { id }, + modalProps: { mediaId: id }, })); }; } diff --git a/app/javascript/flavours/glitch/actions/compose_typed.ts b/app/javascript/flavours/glitch/actions/compose_typed.ts new file mode 100644 index 00000000000000..70be8abcd2160d --- /dev/null +++ b/app/javascript/flavours/glitch/actions/compose_typed.ts @@ -0,0 +1,70 @@ +import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import { apiUpdateMedia } from 'flavours/glitch/api/compose'; +import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments'; +import type { MediaAttachment } from 'flavours/glitch/models/media_attachment'; +import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions'; + +type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { + unattached?: boolean; +}; + +const simulateModifiedApiResponse = ( + media: MediaAttachment, + params: { description?: string; focus?: string }, +): SimulatedMediaAttachmentJSON => { + const [x, y] = (params.focus ?? '').split(','); + + const data = { + ...media.toJS(), + ...params, + meta: { + focus: { + x: parseFloat(x ?? '0'), + y: parseFloat(y ?? '0'), + }, + }, + } as unknown as SimulatedMediaAttachmentJSON; + + return data; +}; + +export const changeUploadCompose = createDataLoadingThunk( + 'compose/changeUpload', + async ( + { + id, + ...params + }: { + id: string; + description: string; + focus: string; + }, + { getState }, + ) => { + const media = ( + (getState().compose as ImmutableMap).get( + 'media_attachments', + ) as ImmutableList + ).find((item) => item.get('id') === id); + + // Editing already-attached media is deferred to editing the post itself. + // For simplicity's sake, fake an API reply. + if (media && !media.get('unattached')) { + return new Promise((resolve) => { + resolve(simulateModifiedApiResponse(media, params)); + }); + } + + return apiUpdateMedia(id, params); + }, + (media: SimulatedMediaAttachmentJSON) => { + return { + media, + attached: typeof media.unattached !== 'undefined' && !media.unattached, + }; + }, + { + useLoadingBar: false, + }, +); diff --git a/app/javascript/flavours/glitch/actions/modal.ts b/app/javascript/flavours/glitch/actions/modal.ts index 01a95051e3c805..9e653c5f4c93f2 100644 --- a/app/javascript/flavours/glitch/actions/modal.ts +++ b/app/javascript/flavours/glitch/actions/modal.ts @@ -9,6 +9,7 @@ export type ModalType = keyof typeof MODAL_COMPONENTS; interface OpenModalPayload { modalType: ModalType; modalProps: ModalProps; + previousModalProps?: ModalProps; } export const openModal = createAction('MODAL_OPEN'); diff --git a/app/javascript/flavours/glitch/api/compose.ts b/app/javascript/flavours/glitch/api/compose.ts new file mode 100644 index 00000000000000..1801ccd67611d7 --- /dev/null +++ b/app/javascript/flavours/glitch/api/compose.ts @@ -0,0 +1,7 @@ +import { apiRequestPut } from 'flavours/glitch/api'; +import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments'; + +export const apiUpdateMedia = ( + id: string, + params?: { description?: string; focus?: string }, +) => apiRequestPut(`v1/media/${id}`, params); diff --git a/app/javascript/flavours/glitch/components/button.tsx b/app/javascript/flavours/glitch/components/button.tsx index b349a83f2bbed2..a527468f6567ed 100644 --- a/app/javascript/flavours/glitch/components/button.tsx +++ b/app/javascript/flavours/glitch/components/button.tsx @@ -7,6 +7,7 @@ interface BaseProps extends Omit, 'children'> { block?: boolean; secondary?: boolean; + compact?: boolean; dangerous?: boolean; } @@ -27,6 +28,7 @@ export const Button: React.FC = ({ disabled, block, secondary, + compact, dangerous, className, title, @@ -47,6 +49,7 @@ export const Button: React.FC = ({ + + + + ); +}; + +const Preview: React.FC<{ + mediaId: string; + position: FocalPoint; + onPositionChange: (arg0: FocalPoint) => void; +}> = ({ mediaId, position, onPositionChange }) => { + const media = useAppSelector((state) => + ( + (state.compose as ImmutableMap).get( + 'media_attachments', + ) as ImmutableList + ).find((x) => x.get('id') === mediaId), + ); + const account = useAppSelector((state) => + me ? state.accounts.get(me) : undefined, + ); + + const [dragging, setDragging] = useState(false); + const [x, y] = position; + const nodeRef = useRef(null); + const draggingRef = useRef(false); + + const setRef = useCallback( + (e: HTMLImageElement | HTMLVideoElement | null) => { + nodeRef.current = e; + }, + [], + ); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button !== 0) { + return; + } + + const { x, y } = getPointerPosition(nodeRef.current, e); + setDragging(true); + draggingRef.current = true; + onPositionChange([x, y]); + }, + [setDragging, onPositionChange], + ); + + const handleTouchStart = useCallback( + (e: React.TouchEvent) => { + const { x, y } = getPointerPosition(nodeRef.current, e); + setDragging(true); + draggingRef.current = true; + onPositionChange([x, y]); + }, + [setDragging, onPositionChange], + ); + + useEffect(() => { + const handleMouseUp = () => { + setDragging(false); + draggingRef.current = false; + }; + + const handleMouseMove = (e: MouseEvent) => { + if (draggingRef.current) { + const { x, y } = getPointerPosition(nodeRef.current, e); + onPositionChange([x, y]); + } + }; + + const handleTouchEnd = () => { + setDragging(false); + draggingRef.current = false; + }; + + const handleTouchMove = (e: TouchEvent) => { + if (draggingRef.current) { + const { x, y } = getPointerPosition(nodeRef.current, e); + onPositionChange([x, y]); + } + }; + + document.addEventListener('mouseup', handleMouseUp); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('touchend', handleTouchEnd); + document.addEventListener('touchmove', handleTouchMove); + + return () => { + document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchmove', handleTouchMove); + }; + }, [setDragging, onPositionChange]); + + if (!media) { + return null; + } + + if (media.get('type') === 'image') { + return ( +
+ +
+
+ ); + } else if (media.get('type') === 'gifv') { + return ( +
+ +
+
+ ); + } else if (media.get('type') === 'video') { + return ( +