From 11786f1114f9dfb5a17810d14477a94162f94064 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 21 Jan 2025 12:34:22 +0100 Subject: [PATCH 1/2] Change design of edit media modal in web UI (#33516) --- app/javascript/images/reticle.png | Bin 1439 -> 0 bytes app/javascript/mastodon/actions/compose.js | 2 +- .../mastodon/actions/compose_typed.ts | 70 +++ app/javascript/mastodon/actions/modal.ts | 1 + app/javascript/mastodon/api/compose.ts | 7 + app/javascript/mastodon/components/button.tsx | 3 + .../mastodon/components/follow_button.tsx | 10 +- app/javascript/mastodon/components/gifv.tsx | 98 ++-- .../features/alt_text_modal/index.tsx | 531 ++++++++++++++++++ .../mastodon/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 +- app/javascript/mastodon/locales/en.json | 21 +- app/javascript/mastodon/reducers/compose.js | 65 +-- app/javascript/mastodon/reducers/index.ts | 4 +- app/javascript/mastodon/reducers/modal.ts | 34 +- .../styles/mastodon-light/diff.scss | 9 +- .../styles/mastodon/components.scss | 232 ++------ app/javascript/styles/mastodon/forms.scss | 29 +- app/javascript/types/tesseract.d.ts | 3 + app/views/admin/accounts/index.html.haml | 2 +- app/views/admin/custom_emojis/index.html.haml | 2 +- .../confirm_suspension.html.haml | 2 +- app/views/admin/instances/index.html.haml | 2 +- app/views/admin/relays/index.html.haml | 2 +- app/views/admin/reports/index.html.haml | 2 +- app/views/admin/tags/index.html.haml | 2 +- .../otp_authentication/show.html.haml | 2 +- .../webauthn_credentials/index.html.haml | 2 +- .../index.html.haml | 2 +- config/webpack/rules/tesseract.js | 5 +- package.json | 2 +- spec/system/ocr_spec.rb | 4 +- yarn.lock | 121 +--- 42 files changed, 919 insertions(+), 900 deletions(-) delete mode 100644 app/javascript/images/reticle.png create mode 100644 app/javascript/mastodon/actions/compose_typed.ts create mode 100644 app/javascript/mastodon/api/compose.ts create mode 100644 app/javascript/mastodon/features/alt_text_modal/index.tsx delete mode 100644 app/javascript/mastodon/features/compose/components/character_counter.jsx create mode 100644 app/javascript/mastodon/features/compose/components/character_counter.tsx delete mode 100644 app/javascript/mastodon/features/ui/components/focal_point_modal.jsx create mode 100644 app/javascript/types/tesseract.d.ts diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png deleted file mode 100644 index a724ac0bcdf53577a4f50e46f5317d196993c5c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1439 zcmeAS@N?(olHy`uVBq!ia0vp^DImLc*Dc-h_F-qbhq1aef135gN4F=3NIVs~-l2IzL}A+d#V$;W zk~5?@t9wQ5H>zxKVC@S!zE=2z|BH+Y^GmYKM+6QpS+tLvD_BP3wFSrJb(3z@ZhE8I zBi25p+{S7`{T;noyWQOPTrha{^OufmS=*Y*fK#X6%{1IBSTpzU&m2Y(1_ov;PZ!6K z3dSS_)*dFeMWRdtt zE4%mKcbrt?k!Zx-lwe|9KkNVJ)i*Ubnr==xo*tiD)_(cvw+Gh!x1WBwmAmTtV`Fh^ zFRzJ_nVpNzylLlZOwf?&RN9@PBExlTL4b|ozd0=;C+8n^k>Of$>S&jjM`)ztwB-un zQ`hZcE4p&!)hh+>+UD(oMW(N$Un$0}S-F$-(^ix1#}q=NtJ$Z#%*byxnfpsm5*2&YLVn0N4B#4yLRp4#QOs5 z^@Yvy@-x~U5;K_Je=3>1V8hDOhiB``W{BMWkXFrZQud}|>T-S7-k|rlKN`JlI`vLY zB4nYH95&K0@xhsCO|CXAcqf*@Cft`6hetxjRz>lx?9cSYD* zu*_YY9Om!m`20&tXueR%r1UjXIt!8wH&n&(TYVK@Bc*uy0K*dlty_~D?O3Z9owfXU z%u&7W*sGJOFJk6xs48H!YCkSzxBJEXLtowt>)z`zmD=}Mv8VCW3sKF(koK3oqAEvI zKZGj(Yg{k3>R|X=rrPK03hS(L`7g28)%>zr@;Hmle!Je1sgG`m|4UVC{o0DPCUW`}|m>)%44ASIzmzcj=C{pHF?}{`QcK z?5;Q0zyD^arhW9<&SEy^c@VB5wI=g|{qzjMF(~)ZQ-*7WhBY!FiF3-?E6se9eA~Bhva@lA0Op z=X={Khfeo5ylGZ3Ww}Xzh4vYJSte(5T0RfkEX)PfbKy(G_5}X7F_Nb6Mw< G&;$T^-EgD; diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index aa1c6de20e..d70834cec6 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -414,7 +414,7 @@ export function initMediaEditModal(id) { dispatch(openModal({ modalType: 'FOCAL_POINT', - modalProps: { id }, + modalProps: { mediaId: id }, })); }; } diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts new file mode 100644 index 0000000000..97f0d68c51 --- /dev/null +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -0,0 +1,70 @@ +import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import { apiUpdateMedia } from 'mastodon/api/compose'; +import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; +import type { MediaAttachment } from 'mastodon/models/media_attachment'; +import { createDataLoadingThunk } from 'mastodon/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/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts index ab03e46765..49af176a11 100644 --- a/app/javascript/mastodon/actions/modal.ts +++ b/app/javascript/mastodon/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/mastodon/api/compose.ts b/app/javascript/mastodon/api/compose.ts new file mode 100644 index 0000000000..757e9961c9 --- /dev/null +++ b/app/javascript/mastodon/api/compose.ts @@ -0,0 +1,7 @@ +import { apiRequestPut } from 'mastodon/api'; +import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; + +export const apiUpdateMedia = ( + id: string, + params?: { description?: string; focus?: string }, +) => apiRequestPut(`v1/media/${id}`, params); diff --git a/app/javascript/mastodon/components/button.tsx b/app/javascript/mastodon/components/button.tsx index b349a83f2b..a527468f65 100644 --- a/app/javascript/mastodon/components/button.tsx +++ b/app/javascript/mastodon/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 ( +