diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 15156e156c..fc6e38fbc8 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -69,3 +69,9 @@ export const apiGetProfile = () => apiRequestGet('v1/profile'); export const apiPatchProfile = (params: ApiProfileUpdateParams | FormData) => apiRequestPatch('v1/profile', params); + +export const apiDeleteProfileAvatar = () => + apiRequestDelete('v1/profile/avatar'); + +export const apiDeleteProfileHeader = () => + apiRequestDelete('v1/profile/header'); diff --git a/app/javascript/mastodon/api_types/profile.ts b/app/javascript/mastodon/api_types/profile.ts index 9814bddde9..acc3b46787 100644 --- a/app/javascript/mastodon/api_types/profile.ts +++ b/app/javascript/mastodon/api_types/profile.ts @@ -27,6 +27,8 @@ export interface ApiProfileJSON { export type ApiProfileUpdateParams = Partial< Pick< ApiProfileJSON, + | 'avatar_description' + | 'header_description' | 'display_name' | 'note' | 'locked' diff --git a/app/javascript/mastodon/components/details/details.stories.tsx b/app/javascript/mastodon/components/details/details.stories.tsx new file mode 100644 index 0000000000..3bc833f313 --- /dev/null +++ b/app/javascript/mastodon/components/details/details.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Details } from './index'; + +const meta = { + component: Details, + title: 'Components/Details', + args: { + summary: 'Here is the summary title', + children: ( +

+ And here are the details that are hidden until you click the summary. +

+ ), + }, + render(props) { + return ( +
+
+
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Plain: Story = {}; diff --git a/app/javascript/mastodon/components/details/index.tsx b/app/javascript/mastodon/components/details/index.tsx new file mode 100644 index 0000000000..aac92e8f77 --- /dev/null +++ b/app/javascript/mastodon/components/details/index.tsx @@ -0,0 +1,35 @@ +import { forwardRef } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; + +import classNames from 'classnames'; + +import ExpandArrowIcon from '@/material-icons/400-24px/expand_more.svg?react'; + +import { Icon } from '../icon'; + +import classes from './styles.module.scss'; + +export const Details = forwardRef< + HTMLDetailsElement, + { + summary: ReactNode; + children: ReactNode; + className?: string; + } & ComponentPropsWithoutRef<'details'> +>(({ summary, children, className, ...rest }, ref) => { + return ( +
+ + {summary} + + + + {children} +
+ ); +}); +Details.displayName = 'Details'; diff --git a/app/javascript/mastodon/components/details/styles.module.scss b/app/javascript/mastodon/components/details/styles.module.scss new file mode 100644 index 0000000000..03aace8a62 --- /dev/null +++ b/app/javascript/mastodon/components/details/styles.module.scss @@ -0,0 +1,25 @@ +.details { + color: var(--color-text-secondary); + font-size: 13px; + margin-top: 8px; + + summary { + cursor: pointer; + font-weight: 600; + list-style: none; + margin-bottom: 8px; + text-decoration: underline; + text-decoration-style: dotted; + } + + :global(.icon) { + width: 1.4em; + height: 1.4em; + vertical-align: middle; + transition: transform 0.2s ease-in-out; + } + + &[open] :global(.icon) { + transform: rotate(-180deg); + } +} diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index a621de4c5d..0157681753 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom'; import type { ModalType } from '@/mastodon/actions/modal'; import { openModal } from '@/mastodon/actions/modal'; +import { AccountBio } from '@/mastodon/components/account_bio'; import { Avatar } from '@/mastodon/components/avatar'; import { Button } from '@/mastodon/components/button'; import { DismissibleCallout } from '@/mastodon/components/callout/dismissible'; @@ -201,7 +202,11 @@ export const AccountEdit: FC = () => { /> } > - + = ({ onClose }) => { - return ; +> = ({ onClose, location }) => { + const { profile, isPending } = useAppSelector((state) => state.profileEdit); + + const initialAlt = profile?.[`${location}Description`]; + const imageSrc = profile?.[`${location}Static`]; + + const [altText, setAltText] = useState(initialAlt ?? ''); + + const dispatch = useAppDispatch(); + const handleSave = useCallback(() => { + void dispatch( + patchProfile({ + [`${location}_description`]: altText, + }), + ).then(onClose); + }, [altText, dispatch, location, onClose]); + + if (!imageSrc) { + return ; + } + + return ( + + ) : ( + + ) + } + onClose={onClose} + onConfirm={handleSave} + confirm={ + + } + updating={isPending} + > +
+ +
+
+ ); +}; + +export const ImageAltTextField: FC<{ + imageSrc: string; + altText: string; + onChange: (altText: string) => void; +}> = ({ imageSrc, altText, onChange }) => { + const altLimit = useAppSelector( + (state) => + state.server.getIn( + ['server', 'configuration', 'media_attachments', 'description_limit'], + 150, + ) as number, + ); + + const handleChange: ChangeEventHandler = useCallback( + (event) => { + onChange(event.currentTarget.value); + }, + [onChange], + ); + + return ( + <> + + +
+ + } + hint={ + + } + onChange={handleChange} + value={altText} + /> + +
+ +
+ } + className={classes.altHint} + > +
    {chunks}
, + li: (chunks) =>
  • {chunks}
  • , + }} + tagName='div' + /> +
    + + ); }; diff --git a/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx b/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx index 211dd35bed..50bcf3d8a1 100644 --- a/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx @@ -1,12 +1,48 @@ +import { useCallback } from 'react'; import type { FC } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { Button } from '@/mastodon/components/button'; +import { deleteImage } from '@/mastodon/reducers/slices/profile_edit'; import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { DialogModal } from '../../ui/components/dialog_modal'; import type { DialogModalProps } from '../../ui/components/dialog_modal'; export const ImageDeleteModal: FC< DialogModalProps & { location: ImageLocation } -> = ({ onClose }) => { - return ; +> = ({ onClose, location }) => { + const isPending = useAppSelector((state) => state.profileEdit.isPending); + const dispatch = useAppDispatch(); + const handleDelete = useCallback(() => { + void dispatch(deleteImage({ location })).then(onClose); + }, [dispatch, location, onClose]); + + return ( + + } + buttons={ + + } + > + + + ); }; diff --git a/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx b/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx index bf6473a9a0..ccf65cceed 100644 --- a/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx @@ -8,9 +8,6 @@ import Cropper from 'react-easy-crop'; import { setDragUploadEnabled } from '@/mastodon/actions/compose_typed'; import { Button } from '@/mastodon/components/button'; -import { Callout } from '@/mastodon/components/callout'; -import { CharacterCounter } from '@/mastodon/components/character_counter'; -import { TextAreaField } from '@/mastodon/components/form_fields'; import { RangeInput } from '@/mastodon/components/form_fields/range_input_field'; import { selectImageInfo, @@ -22,6 +19,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { DialogModal } from '../../ui/components/dialog_modal'; import type { DialogModalProps } from '../../ui/components/dialog_modal'; +import { ImageAltTextField } from './image_alt'; import classes from './styles.module.scss'; import 'react-easy-crop/react-easy-crop.css'; @@ -357,66 +355,19 @@ const StepAlt: FC<{ }> = ({ imageBlob, onCancel, onComplete }) => { const [altText, setAltText] = useState(''); - const handleChange: ChangeEventHandler = useCallback( - (event) => { - setAltText(event.currentTarget.value); - }, - [], - ); - const handleComplete = useCallback(() => { onComplete(altText); }, [altText, onComplete]); const imageSrc = useMemo(() => URL.createObjectURL(imageBlob), [imageBlob]); - const altLimit = useAppSelector( - (state) => - state.server.getIn( - ['server', 'configuration', 'media_attachments', 'description_limit'], - 150, - ) as number, - ); return ( <> - - -
    - - } - hint={ - - } - onChange={handleChange} - /> - -
    - - - } - > - - +