mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 11:11:11 +02:00
Profile editing: Finish image editing (#38235)
This commit is contained in:
@@ -69,3 +69,9 @@ export const apiGetProfile = () => apiRequestGet<ApiProfileJSON>('v1/profile');
|
||||
|
||||
export const apiPatchProfile = (params: ApiProfileUpdateParams | FormData) =>
|
||||
apiRequestPatch<ApiProfileJSON>('v1/profile', params);
|
||||
|
||||
export const apiDeleteProfileAvatar = () =>
|
||||
apiRequestDelete('v1/profile/avatar');
|
||||
|
||||
export const apiDeleteProfileHeader = () =>
|
||||
apiRequestDelete('v1/profile/header');
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface ApiProfileJSON {
|
||||
export type ApiProfileUpdateParams = Partial<
|
||||
Pick<
|
||||
ApiProfileJSON,
|
||||
| 'avatar_description'
|
||||
| 'header_description'
|
||||
| 'display_name'
|
||||
| 'note'
|
||||
| 'locked'
|
||||
|
||||
@@ -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: (
|
||||
<p>
|
||||
And here are the details that are hidden until you click the summary.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
render(props) {
|
||||
return (
|
||||
<div style={{ width: '400px' }}>
|
||||
<Details {...props} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
} satisfies Meta<typeof Details>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Plain: Story = {};
|
||||
35
app/javascript/mastodon/components/details/index.tsx
Normal file
35
app/javascript/mastodon/components/details/index.tsx
Normal file
@@ -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 (
|
||||
<details
|
||||
ref={ref}
|
||||
className={classNames(classes.details, className)}
|
||||
{...rest}
|
||||
>
|
||||
<summary>
|
||||
{summary}
|
||||
<Icon icon={ExpandArrowIcon} id='arrow' />
|
||||
</summary>
|
||||
|
||||
{children}
|
||||
</details>
|
||||
);
|
||||
});
|
||||
Details.displayName = 'Details';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EmojiHTML htmlString={profile.bio} {...htmlHandlers} />
|
||||
<AccountBio
|
||||
showDropdown
|
||||
accountId={profile.id}
|
||||
className={classes.bio}
|
||||
/>
|
||||
</AccountEditSection>
|
||||
|
||||
<AccountEditSection
|
||||
|
||||
@@ -1,12 +1,147 @@
|
||||
import type { FC } from 'react';
|
||||
import type { ChangeEventHandler, FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { CharacterCounter } from '@/mastodon/components/character_counter';
|
||||
import { Details } from '@/mastodon/components/details';
|
||||
import { TextAreaField } from '@/mastodon/components/form_fields';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { patchProfile } 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 { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const ImageAltModal: FC<
|
||||
DialogModalProps & { location: ImageLocation }
|
||||
> = ({ onClose }) => {
|
||||
return <DialogModal title='TODO' onClose={onClose} />;
|
||||
> = ({ 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 <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={
|
||||
initialAlt ? (
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.edit_title'
|
||||
defaultMessage='Edit alt text'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.add_title'
|
||||
defaultMessage='Add alt text'
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={onClose}
|
||||
onConfirm={handleSave}
|
||||
confirm={
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.done'
|
||||
defaultMessage='Done'
|
||||
/>
|
||||
}
|
||||
updating={isPending}
|
||||
>
|
||||
<div className={classes.wrapper}>
|
||||
<ImageAltTextField
|
||||
imageSrc={imageSrc}
|
||||
altText={altText}
|
||||
onChange={setAltText}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
|
||||
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<HTMLTextAreaElement> = useCallback(
|
||||
(event) => {
|
||||
onChange(event.currentTarget.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<img src={imageSrc} alt='' className={classes.altImage} />
|
||||
|
||||
<div>
|
||||
<TextAreaField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.text_label'
|
||||
defaultMessage='Alt text'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.text_hint'
|
||||
defaultMessage='Alt text helps screen reader users to understand your content.'
|
||||
/>
|
||||
}
|
||||
onChange={handleChange}
|
||||
value={altText}
|
||||
/>
|
||||
<CharacterCounter
|
||||
currentString={altText}
|
||||
maxLength={altLimit}
|
||||
className={classes.altCounter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Details
|
||||
summary={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.details_title'
|
||||
defaultMessage='Tips: Alt text for profile photos'
|
||||
/>
|
||||
}
|
||||
className={classes.altHint}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.details_content'
|
||||
defaultMessage='DO: <ul> <li>Describe yourself as pictured</li> <li>Use third person language (e.g. “Alex” instead of “me”)</li> <li>Be succinct – a few words is often enough</li> </ul> DON’T: <ul> <li>Start with “Photo of” – it’s redundant for screen readers</li> </ul> EXAMPLE: <ul> <li>“Alex wearing a green shirt and glasses”</li> </ul>'
|
||||
values={{
|
||||
ul: (chunks) => <ul>{chunks}</ul>,
|
||||
li: (chunks) => <li>{chunks}</li>,
|
||||
}}
|
||||
tagName='div'
|
||||
/>
|
||||
</Details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 <DialogModal title='TODO' onClose={onClose} />;
|
||||
> = ({ 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 (
|
||||
<DialogModal
|
||||
onClose={onClose}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_delete_modal.title'
|
||||
defaultMessage='Delete image?'
|
||||
/>
|
||||
}
|
||||
buttons={
|
||||
<Button dangerous onClick={handleDelete} disabled={isPending}>
|
||||
<FormattedMessage
|
||||
id='account_edit.image_delete_modal.delete_button'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='account_edit.image_delete_modal.confirm'
|
||||
defaultMessage='Are you sure you want to delete this image? This action can’t be undone.'
|
||||
tagName='p'
|
||||
/>
|
||||
</DialogModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<HTMLTextAreaElement> = 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 (
|
||||
<>
|
||||
<img src={imageSrc} alt='' className={classes.altImage} />
|
||||
|
||||
<div>
|
||||
<TextAreaField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.step_alt.text_label'
|
||||
defaultMessage='Alt text'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.step_alt.text_hint'
|
||||
defaultMessage='E.g. “Close-up photo of me wearing glasses and a blue shirt”'
|
||||
/>
|
||||
}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<CharacterCounter
|
||||
currentString={altText}
|
||||
maxLength={altLimit}
|
||||
className={classes.altCounter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.step_alt.callout_title'
|
||||
defaultMessage='Let’s make Mastodon accessible for all'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.step_alt.callout_text'
|
||||
defaultMessage='Adding alt text to media helps people using screen readers to understand your content.'
|
||||
/>
|
||||
</Callout>
|
||||
<ImageAltTextField
|
||||
imageSrc={imageSrc}
|
||||
altText={altText}
|
||||
onChange={setAltText}
|
||||
/>
|
||||
|
||||
<div className={classes.cropActions}>
|
||||
<Button onClick={onCancel} secondary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './bio_modal';
|
||||
export * from './fields_modals';
|
||||
export * from './fields_reorder_modal';
|
||||
export * from './image_alt';
|
||||
export { ImageAltModal } from './image_alt';
|
||||
export * from './image_delete';
|
||||
export * from './image_upload';
|
||||
export * from './name_modal';
|
||||
|
||||
@@ -121,14 +121,25 @@
|
||||
}
|
||||
|
||||
.altImage {
|
||||
max-height: 300px;
|
||||
max-height: 150px;
|
||||
object-fit: contain;
|
||||
align-self: flex-start;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--avatar-border-radius);
|
||||
}
|
||||
|
||||
.altCounter {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.altHint {
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
list-style: disc;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.verifiedSteps {
|
||||
font-size: 15px;
|
||||
|
||||
@@ -157,29 +168,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Details } from '@/mastodon/components/details';
|
||||
import { CopyLinkField } from '@/mastodon/components/form_fields/copy_link_field';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||
import ExpandArrowIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
||||
|
||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||
@@ -53,20 +52,20 @@ export const VerifiedModal: FC<DialogModalProps> = ({ onClose }) => {
|
||||
}
|
||||
value={`<a rel="me" href="${accountUrl}">Mastodon</a>`}
|
||||
/>
|
||||
<details className={classes.details}>
|
||||
<summary>
|
||||
<Details
|
||||
summary={
|
||||
<FormattedMessage
|
||||
id='account_edit.verified_modal.invisible_link.summary'
|
||||
defaultMessage='How do I make the link invisible?'
|
||||
/>
|
||||
<Icon icon={ExpandArrowIcon} id='arrow' />
|
||||
</summary>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='account_edit.verified_modal.invisible_link.details'
|
||||
defaultMessage='Add the link to your header. The important part is rel="me" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.'
|
||||
values={{ tag: <code><a></code> }}
|
||||
/>
|
||||
</details>
|
||||
</Details>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -40,6 +40,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bio {
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
|
||||
@@ -211,6 +211,7 @@ export const AccountHeader: React.FC<{
|
||||
))}
|
||||
|
||||
<AccountBio
|
||||
showDropdown
|
||||
accountId={accountId}
|
||||
className={classNames(
|
||||
'account__header__content',
|
||||
|
||||
@@ -184,6 +184,15 @@
|
||||
"account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".",
|
||||
"account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"",
|
||||
"account_edit.field_reorder_modal.title": "Rearrange fields",
|
||||
"account_edit.image_alt_modal.add_title": "Add alt text",
|
||||
"account_edit.image_alt_modal.details_content": "DO: <ul> <li>Describe yourself as pictured</li> <li>Use third person language (e.g. “Alex” instead of “me”)</li> <li>Be succinct – a few words is often enough</li> </ul> DON’T: <ul> <li>Start with “Photo of” – it’s redundant for screen readers</li> </ul> EXAMPLE: <ul> <li>“Alex wearing a green shirt and glasses”</li> </ul>",
|
||||
"account_edit.image_alt_modal.details_title": "Tips: Alt text for profile photos",
|
||||
"account_edit.image_alt_modal.edit_title": "Edit alt text",
|
||||
"account_edit.image_alt_modal.text_hint": "Alt text helps screen reader users to understand your content.",
|
||||
"account_edit.image_alt_modal.text_label": "Alt text",
|
||||
"account_edit.image_delete_modal.confirm": "Are you sure you want to delete this image? This action can’t be undone.",
|
||||
"account_edit.image_delete_modal.delete_button": "Delete",
|
||||
"account_edit.image_delete_modal.title": "Delete image?",
|
||||
"account_edit.image_edit.add_button": "Add image",
|
||||
"account_edit.image_edit.alt_add_button": "Add alt text",
|
||||
"account_edit.image_edit.alt_edit_button": "Edit alt text",
|
||||
@@ -206,10 +215,6 @@
|
||||
"account_edit.upload_modal.back": "Back",
|
||||
"account_edit.upload_modal.done": "Done",
|
||||
"account_edit.upload_modal.next": "Next",
|
||||
"account_edit.upload_modal.step_alt.callout_text": "Adding alt text to media helps people using screen readers to understand your content.",
|
||||
"account_edit.upload_modal.step_alt.callout_title": "Let’s make Mastodon accessible for all",
|
||||
"account_edit.upload_modal.step_alt.text_hint": "E.g. “Close-up photo of me wearing glasses and a blue shirt”",
|
||||
"account_edit.upload_modal.step_alt.text_label": "Alt text",
|
||||
"account_edit.upload_modal.step_crop.zoom": "Zoom",
|
||||
"account_edit.upload_modal.step_upload.button": "Browse files",
|
||||
"account_edit.upload_modal.step_upload.dragging": "Drop to upload",
|
||||
|
||||
@@ -3,8 +3,11 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { fetchAccount } from '@/mastodon/actions/accounts';
|
||||
import {
|
||||
apiDeleteFeaturedTag,
|
||||
apiDeleteProfileAvatar,
|
||||
apiDeleteProfileHeader,
|
||||
apiGetCurrentFeaturedTags,
|
||||
apiGetProfile,
|
||||
apiGetTagSuggestions,
|
||||
@@ -120,6 +123,16 @@ const profileEditSlice = createSlice({
|
||||
state.isPending = false;
|
||||
});
|
||||
|
||||
builder.addCase(deleteImage.pending, (state) => {
|
||||
state.isPending = true;
|
||||
});
|
||||
builder.addCase(deleteImage.rejected, (state) => {
|
||||
state.isPending = false;
|
||||
});
|
||||
builder.addCase(deleteImage.fulfilled, (state) => {
|
||||
state.isPending = false;
|
||||
});
|
||||
|
||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
||||
state.isPending = true;
|
||||
});
|
||||
@@ -231,7 +244,10 @@ export const fetchProfile = createDataLoadingThunk(
|
||||
export const patchProfile = createDataLoadingThunk(
|
||||
`${profileEditSlice.name}/patchProfile`,
|
||||
(params: Partial<ApiProfileUpdateParams>) => apiPatchProfile(params),
|
||||
transformProfile,
|
||||
(response, { dispatch }) => {
|
||||
dispatch(fetchAccount(response.id));
|
||||
return transformProfile(response);
|
||||
},
|
||||
{
|
||||
useLoadingBar: false,
|
||||
condition(_, { getState }) {
|
||||
@@ -263,13 +279,39 @@ export const selectImageInfo = createAppSelector(
|
||||
export const uploadImage = createDataLoadingThunk(
|
||||
`${profileEditSlice.name}/uploadImage`,
|
||||
(arg: { location: ImageLocation; imageBlob: Blob; altText: string }) => {
|
||||
// Note: Alt text is not actually supported by the API yet.
|
||||
const formData = new FormData();
|
||||
formData.append(arg.location, arg.imageBlob);
|
||||
if (arg.altText) {
|
||||
formData.append(`${arg.location}_description`, arg.altText);
|
||||
}
|
||||
|
||||
return apiPatchProfile(formData);
|
||||
},
|
||||
transformProfile,
|
||||
(response, { dispatch }) => {
|
||||
dispatch(fetchAccount(response.id));
|
||||
return transformProfile(response);
|
||||
},
|
||||
{
|
||||
useLoadingBar: false,
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteImage = createDataLoadingThunk(
|
||||
`${profileEditSlice.name}/deleteImage`,
|
||||
(arg: { location: ImageLocation }) => {
|
||||
if (arg.location === 'avatar') {
|
||||
return apiDeleteProfileAvatar();
|
||||
} else {
|
||||
return apiDeleteProfileHeader();
|
||||
}
|
||||
},
|
||||
async (_, { dispatch, getState }) => {
|
||||
await dispatch(fetchProfile());
|
||||
const accountId = getState().profileEdit.profile?.id;
|
||||
if (accountId) {
|
||||
dispatch(fetchAccount(accountId));
|
||||
}
|
||||
},
|
||||
{
|
||||
useLoadingBar: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user