Profile editing: Utilize new API (#37990)
This commit is contained in:
@@ -179,3 +179,10 @@ export async function apiRequestDelete<
|
|||||||
>(url: ApiUrl, params?: RequestParamsOrData<ApiParams>) {
|
>(url: ApiUrl, params?: RequestParamsOrData<ApiParams>) {
|
||||||
return apiRequest<ApiResponse>('DELETE', url, { params });
|
return apiRequest<ApiResponse>('DELETE', url, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function apiRequestPatch<ApiResponse = unknown, ApiData = unknown>(
|
||||||
|
url: ApiUrl,
|
||||||
|
data?: RequestParamsOrData<ApiData>,
|
||||||
|
) {
|
||||||
|
return apiRequest<ApiResponse>('PATCH', url, { data });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { apiRequestPost, apiRequestGet, apiRequestDelete } from 'mastodon/api';
|
import {
|
||||||
|
apiRequestPost,
|
||||||
|
apiRequestGet,
|
||||||
|
apiRequestDelete,
|
||||||
|
apiRequestPatch,
|
||||||
|
} from 'mastodon/api';
|
||||||
import type {
|
import type {
|
||||||
ApiAccountJSON,
|
ApiAccountJSON,
|
||||||
ApiFamiliarFollowersJSON,
|
ApiFamiliarFollowersJSON,
|
||||||
@@ -9,6 +14,11 @@ import type {
|
|||||||
ApiHashtagJSON,
|
ApiHashtagJSON,
|
||||||
} from 'mastodon/api_types/tags';
|
} from 'mastodon/api_types/tags';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiProfileJSON,
|
||||||
|
ApiProfileUpdateParams,
|
||||||
|
} from '../api_types/profile';
|
||||||
|
|
||||||
export const apiSubmitAccountNote = (id: string, value: string) =>
|
export const apiSubmitAccountNote = (id: string, value: string) =>
|
||||||
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
||||||
comment: value,
|
comment: value,
|
||||||
@@ -54,3 +64,8 @@ export const apiGetFamiliarFollowers = (id: string) =>
|
|||||||
apiRequestGet<ApiFamiliarFollowersJSON>('v1/accounts/familiar_followers', {
|
apiRequestGet<ApiFamiliarFollowersJSON>('v1/accounts/familiar_followers', {
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiGetProfile = () => apiRequestGet<ApiProfileJSON>('v1/profile');
|
||||||
|
|
||||||
|
export const apiPatchProfile = (params: ApiProfileUpdateParams) =>
|
||||||
|
apiRequestPatch<ApiProfileJSON>('v1/profile', params);
|
||||||
|
|||||||
42
app/javascript/mastodon/api_types/profile.ts
Normal file
42
app/javascript/mastodon/api_types/profile.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ApiAccountFieldJSON } from './accounts';
|
||||||
|
|
||||||
|
export interface ApiProfileJSON {
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
note: string;
|
||||||
|
fields: ApiAccountFieldJSON[];
|
||||||
|
avatar: string;
|
||||||
|
avatar_static: string;
|
||||||
|
avatar_description: string;
|
||||||
|
header: string;
|
||||||
|
header_static: string;
|
||||||
|
header_description: string;
|
||||||
|
locked: boolean;
|
||||||
|
bot: boolean;
|
||||||
|
hide_collections: boolean;
|
||||||
|
discoverable: boolean;
|
||||||
|
indexable: boolean;
|
||||||
|
show_media: boolean;
|
||||||
|
show_media_replies: boolean;
|
||||||
|
show_featured: boolean;
|
||||||
|
attribution_domains: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiProfileUpdateParams = Partial<
|
||||||
|
Pick<
|
||||||
|
ApiProfileJSON,
|
||||||
|
| 'display_name'
|
||||||
|
| 'note'
|
||||||
|
| 'locked'
|
||||||
|
| 'bot'
|
||||||
|
| 'hide_collections'
|
||||||
|
| 'discoverable'
|
||||||
|
| 'indexable'
|
||||||
|
| 'show_media'
|
||||||
|
| 'show_media_replies'
|
||||||
|
| 'show_featured'
|
||||||
|
>
|
||||||
|
> & {
|
||||||
|
attribution_domains?: string[];
|
||||||
|
fields_attributes?: Pick<ApiAccountFieldJSON, 'name' | 'value'>[];
|
||||||
|
};
|
||||||
@@ -4,12 +4,11 @@ import type { ChangeEventHandler, FC } from 'react';
|
|||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { TextArea } from '@/mastodon/components/form_fields';
|
import { TextArea } from '@/mastodon/components/form_fields';
|
||||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
|
||||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||||
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||||
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import classes from '../styles.module.scss';
|
import classes from '../styles.module.scss';
|
||||||
|
|
||||||
@@ -38,10 +37,11 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
|||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
const counterId = useId();
|
const counterId = useId();
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const accountId = useCurrentAccountId();
|
|
||||||
const account = useAccount(accountId);
|
|
||||||
|
|
||||||
const [newBio, setNewBio] = useState(account?.note_plain ?? '');
|
const { profile: { bio } = {}, isPending } = useAppSelector(
|
||||||
|
(state) => state.profileEdit,
|
||||||
|
);
|
||||||
|
const [newBio, setNewBio] = useState(bio ?? '');
|
||||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
setNewBio(event.currentTarget.value);
|
setNewBio(event.currentTarget.value);
|
||||||
@@ -55,19 +55,22 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!account) {
|
const dispatch = useAppDispatch();
|
||||||
return <LoadingIndicator />;
|
const handleSave = useCallback(() => {
|
||||||
}
|
if (!isPending) {
|
||||||
|
void dispatch(patchProfile({ note: newBio })).then(onClose);
|
||||||
|
}
|
||||||
|
}, [dispatch, isPending, newBio, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
title={intl.formatMessage(
|
title={intl.formatMessage(bio ? messages.editTitle : messages.addTitle)}
|
||||||
account.note_plain ? messages.editTitle : messages.addTitle,
|
|
||||||
)}
|
|
||||||
titleId={titleId}
|
titleId={titleId}
|
||||||
confirm={intl.formatMessage(messages.save)}
|
confirm={intl.formatMessage(messages.save)}
|
||||||
onConfirm={onClose} // To be implemented
|
onConfirm={handleSave}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
updating={isPending}
|
||||||
|
disabled={newBio.length > MAX_BIO_LENGTH}
|
||||||
noFocusButton
|
noFocusButton
|
||||||
>
|
>
|
||||||
<div className={classes.inputWrapper}>
|
<div className={classes.inputWrapper}>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { TextInput } from '@/mastodon/components/form_fields';
|
|||||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||||
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||||
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import classes from '../styles.module.scss';
|
import classes from '../styles.module.scss';
|
||||||
|
|
||||||
@@ -37,10 +37,11 @@ export const NameModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
|||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
const counterId = useId();
|
const counterId = useId();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const accountId = useCurrentAccountId();
|
|
||||||
const account = useAccount(accountId);
|
|
||||||
|
|
||||||
const [newName, setNewName] = useState(account?.display_name ?? '');
|
const { profile: { displayName } = {}, isPending } = useAppSelector(
|
||||||
|
(state) => state.profileEdit,
|
||||||
|
);
|
||||||
|
const [newName, setNewName] = useState(displayName ?? '');
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
setNewName(event.currentTarget.value);
|
setNewName(event.currentTarget.value);
|
||||||
@@ -54,13 +55,22 @@ export const NameModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
if (!isPending) {
|
||||||
|
void dispatch(patchProfile({ display_name: newName })).then(onClose);
|
||||||
|
}
|
||||||
|
}, [dispatch, isPending, newName, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
title={intl.formatMessage(messages.editTitle)}
|
title={intl.formatMessage(messages.editTitle)}
|
||||||
titleId={titleId}
|
titleId={titleId}
|
||||||
confirm={intl.formatMessage(messages.save)}
|
confirm={intl.formatMessage(messages.save)}
|
||||||
onConfirm={onClose} // To be implemented
|
onConfirm={handleSave}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
updating={isPending}
|
||||||
|
disabled={newName.length > MAX_NAME_LENGTH}
|
||||||
noCloseOnConfirm
|
noCloseOnConfirm
|
||||||
noFocusButton
|
noFocusButton
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import {
|
|||||||
fetchFeaturedTags,
|
fetchFeaturedTags,
|
||||||
fetchSuggestedTags,
|
fetchSuggestedTags,
|
||||||
} from '@/mastodon/reducers/slices/profile_edit';
|
} from '@/mastodon/reducers/slices/profile_edit';
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '@/mastodon/store';
|
||||||
|
|
||||||
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
||||||
import { AccountEditItemList } from './components/item_list';
|
import { AccountEditItemList } from './components/item_list';
|
||||||
@@ -28,14 +32,23 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectTags = createAppSelector(
|
||||||
|
[(state) => state.profileEdit],
|
||||||
|
(profileEdit) => ({
|
||||||
|
tags: profileEdit.tags ?? [],
|
||||||
|
tagSuggestions: profileEdit.tagSuggestions ?? [],
|
||||||
|
isLoading: !profileEdit.tags || !profileEdit.tagSuggestions,
|
||||||
|
isPending: profileEdit.isPending,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const AccountEditFeaturedTags: FC = () => {
|
export const AccountEditFeaturedTags: FC = () => {
|
||||||
const accountId = useCurrentAccountId();
|
const accountId = useCurrentAccountId();
|
||||||
const account = useAccount(accountId);
|
const account = useAccount(accountId);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { tags, tagSuggestions, isLoading, isPending } = useAppSelector(
|
const { tags, tagSuggestions, isLoading, isPending } =
|
||||||
(state) => state.profileEdit,
|
useAppSelector(selectTags);
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import { useHistory } from 'react-router-dom';
|
|||||||
|
|
||||||
import type { ModalType } from '@/mastodon/actions/modal';
|
import type { ModalType } from '@/mastodon/actions/modal';
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
|
||||||
import { Avatar } from '@/mastodon/components/avatar';
|
import { Avatar } from '@/mastodon/components/avatar';
|
||||||
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
|
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||||
import { fetchFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
|
import {
|
||||||
|
fetchFeaturedTags,
|
||||||
|
fetchProfile,
|
||||||
|
} from '@/mastodon/reducers/slices/profile_edit';
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
||||||
@@ -82,11 +86,10 @@ export const AccountEdit: FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { tags: featuredTags, isLoading: isTagsLoading } = useAppSelector(
|
const { profile, tags = [] } = useAppSelector((state) => state.profileEdit);
|
||||||
(state) => state.profileEdit,
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void dispatch(fetchFeaturedTags());
|
void dispatch(fetchFeaturedTags());
|
||||||
|
void dispatch(fetchProfile());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleOpenModal = useCallback(
|
const handleOpenModal = useCallback(
|
||||||
@@ -107,14 +110,20 @@ export const AccountEdit: FC = () => {
|
|||||||
history.push('/profile/featured_tags');
|
history.push('/profile/featured_tags');
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
if (!accountId || !account) {
|
// Normally we would use the account emoji, but we want all custom emojis to be available to render after editing.
|
||||||
|
const emojis = useAppSelector((state) => state.custom_emojis);
|
||||||
|
const htmlHandlers = useElementHandledLink({
|
||||||
|
hashtagAccountId: profile?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!accountId || !account || !profile) {
|
||||||
return <AccountEditEmptyColumn notFound={!accountId} />;
|
return <AccountEditEmptyColumn notFound={!accountId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerSrc = autoPlayGif ? account.header : account.header_static;
|
const headerSrc = autoPlayGif ? profile.header : profile.headerStatic;
|
||||||
const hasName = !!account.display_name;
|
const hasName = !!profile.displayName;
|
||||||
const hasBio = !!account.note_plain;
|
const hasBio = !!profile.bio;
|
||||||
const hasTags = !isTagsLoading && featuredTags.length > 0;
|
const hasTags = tags.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountEditColumn
|
<AccountEditColumn
|
||||||
@@ -128,62 +137,64 @@ export const AccountEdit: FC = () => {
|
|||||||
<Avatar account={account} size={80} className={classes.avatar} />
|
<Avatar account={account} size={80} className={classes.avatar} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<AccountEditSection
|
<CustomEmojiProvider emojis={emojis}>
|
||||||
title={messages.displayNameTitle}
|
<AccountEditSection
|
||||||
description={messages.displayNamePlaceholder}
|
title={messages.displayNameTitle}
|
||||||
showDescription={!hasName}
|
description={messages.displayNamePlaceholder}
|
||||||
buttons={
|
showDescription={!hasName}
|
||||||
<EditButton
|
buttons={
|
||||||
onClick={handleNameEdit}
|
<EditButton
|
||||||
item={messages.displayNameTitle}
|
onClick={handleNameEdit}
|
||||||
edit={hasName}
|
item={messages.displayNameTitle}
|
||||||
/>
|
edit={hasName}
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
<DisplayNameSimple account={account} />
|
>
|
||||||
</AccountEditSection>
|
<EmojiHTML htmlString={profile.displayName} {...htmlHandlers} />
|
||||||
|
</AccountEditSection>
|
||||||
|
|
||||||
<AccountEditSection
|
<AccountEditSection
|
||||||
title={messages.bioTitle}
|
title={messages.bioTitle}
|
||||||
description={messages.bioPlaceholder}
|
description={messages.bioPlaceholder}
|
||||||
showDescription={!hasBio}
|
showDescription={!hasBio}
|
||||||
buttons={
|
buttons={
|
||||||
<EditButton
|
<EditButton
|
||||||
onClick={handleBioEdit}
|
onClick={handleBioEdit}
|
||||||
item={messages.bioTitle}
|
item={messages.bioTitle}
|
||||||
edit={hasBio}
|
edit={hasBio}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AccountBio accountId={accountId} />
|
<EmojiHTML htmlString={profile.bio} {...htmlHandlers} />
|
||||||
</AccountEditSection>
|
</AccountEditSection>
|
||||||
|
|
||||||
<AccountEditSection
|
<AccountEditSection
|
||||||
title={messages.customFieldsTitle}
|
title={messages.customFieldsTitle}
|
||||||
description={messages.customFieldsPlaceholder}
|
description={messages.customFieldsPlaceholder}
|
||||||
showDescription
|
showDescription
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AccountEditSection
|
<AccountEditSection
|
||||||
title={messages.featuredHashtagsTitle}
|
title={messages.featuredHashtagsTitle}
|
||||||
description={messages.featuredHashtagsPlaceholder}
|
description={messages.featuredHashtagsPlaceholder}
|
||||||
showDescription={!hasTags}
|
showDescription={!hasTags}
|
||||||
buttons={
|
buttons={
|
||||||
<EditButton
|
<EditButton
|
||||||
onClick={handleFeaturedTagsEdit}
|
onClick={handleFeaturedTagsEdit}
|
||||||
edit={hasTags}
|
edit={hasTags}
|
||||||
item={messages.featuredHashtagsItem}
|
item={messages.featuredHashtagsItem}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{featuredTags.map((tag) => `#${tag.name}`).join(', ')}
|
{tags.map((tag) => `#${tag.name}`).join(', ')}
|
||||||
</AccountEditSection>
|
</AccountEditSection>
|
||||||
|
|
||||||
<AccountEditSection
|
<AccountEditSection
|
||||||
title={messages.profileTabTitle}
|
title={messages.profileTabTitle}
|
||||||
description={messages.profileTabSubtitle}
|
description={messages.profileTabSubtitle}
|
||||||
showDescription
|
showDescription
|
||||||
/>
|
/>
|
||||||
|
</CustomEmojiProvider>
|
||||||
</AccountEditColumn>
|
</AccountEditColumn>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
|
|||||||
import { buildCustomEmojis } from '../features/emoji/emoji';
|
import { buildCustomEmojis } from '../features/emoji/emoji';
|
||||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
||||||
|
|
||||||
|
/** @type {ImmutableList<import('@/mastodon/models/custom_emoji').CustomEmoji>} */
|
||||||
const initialState = ImmutableList([]);
|
const initialState = ImmutableList([]);
|
||||||
|
|
||||||
export default function custom_emojis(state = initialState, action) {
|
export default function custom_emojis(state = initialState, action) {
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import { debounce } from 'lodash';
|
|||||||
import {
|
import {
|
||||||
apiDeleteFeaturedTag,
|
apiDeleteFeaturedTag,
|
||||||
apiGetCurrentFeaturedTags,
|
apiGetCurrentFeaturedTags,
|
||||||
|
apiGetProfile,
|
||||||
apiGetTagSuggestions,
|
apiGetTagSuggestions,
|
||||||
|
apiPatchProfile,
|
||||||
apiPostFeaturedTag,
|
apiPostFeaturedTag,
|
||||||
} from '@/mastodon/api/accounts';
|
} from '@/mastodon/api/accounts';
|
||||||
import { apiGetSearch } from '@/mastodon/api/search';
|
import { apiGetSearch } from '@/mastodon/api/search';
|
||||||
|
import type {
|
||||||
|
ApiProfileJSON,
|
||||||
|
ApiProfileUpdateParams,
|
||||||
|
} from '@/mastodon/api_types/profile';
|
||||||
import { hashtagToFeaturedTag } from '@/mastodon/api_types/tags';
|
import { hashtagToFeaturedTag } from '@/mastodon/api_types/tags';
|
||||||
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
|
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
|
||||||
import type { AppDispatch } from '@/mastodon/store';
|
import type { AppDispatch } from '@/mastodon/store';
|
||||||
@@ -17,11 +23,21 @@ import {
|
|||||||
createAppAsyncThunk,
|
createAppAsyncThunk,
|
||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
} from '@/mastodon/store/typed_functions';
|
} from '@/mastodon/store/typed_functions';
|
||||||
|
import type { SnakeToCamelCase } from '@/mastodon/utils/types';
|
||||||
|
|
||||||
interface ProfileEditState {
|
type ProfileData = {
|
||||||
tags: ApiFeaturedTagJSON[];
|
[Key in keyof Omit<
|
||||||
tagSuggestions: ApiFeaturedTagJSON[];
|
ApiProfileJSON,
|
||||||
isLoading: boolean;
|
'note'
|
||||||
|
> as SnakeToCamelCase<Key>]: ApiProfileJSON[Key];
|
||||||
|
} & {
|
||||||
|
bio: ApiProfileJSON['note'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProfileEditState {
|
||||||
|
profile?: ProfileData;
|
||||||
|
tags?: ApiFeaturedTagJSON[];
|
||||||
|
tagSuggestions?: ApiFeaturedTagJSON[];
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
search: {
|
search: {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -31,9 +47,6 @@ interface ProfileEditState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ProfileEditState = {
|
const initialState: ProfileEditState = {
|
||||||
tags: [],
|
|
||||||
tagSuggestions: [],
|
|
||||||
isLoading: true,
|
|
||||||
isPending: false,
|
isPending: false,
|
||||||
search: {
|
search: {
|
||||||
query: '',
|
query: '',
|
||||||
@@ -49,6 +62,7 @@ const profileEditSlice = createSlice({
|
|||||||
if (state.search.query === action.payload) {
|
if (state.search.query === action.payload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.search.query = action.payload;
|
state.search.query = action.payload;
|
||||||
state.search.isLoading = false;
|
state.search.isLoading = false;
|
||||||
state.search.results = undefined;
|
state.search.results = undefined;
|
||||||
@@ -60,13 +74,25 @@ const profileEditSlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
|
builder.addCase(fetchProfile.fulfilled, (state, action) => {
|
||||||
|
state.profile = action.payload;
|
||||||
|
});
|
||||||
builder.addCase(fetchSuggestedTags.fulfilled, (state, action) => {
|
builder.addCase(fetchSuggestedTags.fulfilled, (state, action) => {
|
||||||
state.tagSuggestions = action.payload.map(hashtagToFeaturedTag);
|
state.tagSuggestions = action.payload.map(hashtagToFeaturedTag);
|
||||||
state.isLoading = false;
|
|
||||||
});
|
});
|
||||||
builder.addCase(fetchFeaturedTags.fulfilled, (state, action) => {
|
builder.addCase(fetchFeaturedTags.fulfilled, (state, action) => {
|
||||||
state.tags = action.payload;
|
state.tags = action.payload;
|
||||||
state.isLoading = false;
|
});
|
||||||
|
|
||||||
|
builder.addCase(patchProfile.pending, (state) => {
|
||||||
|
state.isPending = true;
|
||||||
|
});
|
||||||
|
builder.addCase(patchProfile.rejected, (state) => {
|
||||||
|
state.isPending = false;
|
||||||
|
});
|
||||||
|
builder.addCase(patchProfile.fulfilled, (state, action) => {
|
||||||
|
state.profile = action.payload;
|
||||||
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
builder.addCase(addFeaturedTag.pending, (state) => {
|
||||||
@@ -76,12 +102,18 @@ const profileEditSlice = createSlice({
|
|||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
||||||
|
if (!state.tags) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.tags = [...state.tags, action.payload].toSorted(
|
state.tags = [...state.tags, action.payload].toSorted(
|
||||||
(a, b) => b.statuses_count - a.statuses_count,
|
(a, b) => b.statuses_count - a.statuses_count,
|
||||||
);
|
);
|
||||||
state.tagSuggestions = state.tagSuggestions.filter(
|
if (state.tagSuggestions) {
|
||||||
(tag) => tag.name !== action.meta.arg.name,
|
state.tagSuggestions = state.tagSuggestions.filter(
|
||||||
);
|
(tag) => tag.name !== action.meta.arg.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +124,10 @@ const profileEditSlice = createSlice({
|
|||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
builder.addCase(deleteFeaturedTag.fulfilled, (state, action) => {
|
builder.addCase(deleteFeaturedTag.fulfilled, (state, action) => {
|
||||||
|
if (!state.tags) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.tags = state.tags.filter((tag) => tag.id !== action.meta.arg.tagId);
|
state.tags = state.tags.filter((tag) => tag.id !== action.meta.arg.tagId);
|
||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
@@ -106,7 +142,7 @@ const profileEditSlice = createSlice({
|
|||||||
builder.addCase(fetchSearchResults.fulfilled, (state, action) => {
|
builder.addCase(fetchSearchResults.fulfilled, (state, action) => {
|
||||||
state.search.isLoading = false;
|
state.search.isLoading = false;
|
||||||
const searchResults: ApiFeaturedTagJSON[] = [];
|
const searchResults: ApiFeaturedTagJSON[] = [];
|
||||||
const currentTags = new Set(state.tags.map((tag) => tag.name));
|
const currentTags = new Set((state.tags ?? []).map((tag) => tag.name));
|
||||||
|
|
||||||
for (const tag of action.payload) {
|
for (const tag of action.payload) {
|
||||||
if (currentTags.has(tag.name)) {
|
if (currentTags.has(tag.name)) {
|
||||||
@@ -125,6 +161,41 @@ const profileEditSlice = createSlice({
|
|||||||
export const profileEdit = profileEditSlice.reducer;
|
export const profileEdit = profileEditSlice.reducer;
|
||||||
export const { clearSearch } = profileEditSlice.actions;
|
export const { clearSearch } = profileEditSlice.actions;
|
||||||
|
|
||||||
|
const transformProfile = (result: ApiProfileJSON): ProfileData => ({
|
||||||
|
id: result.id,
|
||||||
|
displayName: result.display_name,
|
||||||
|
bio: result.note,
|
||||||
|
fields: result.fields,
|
||||||
|
avatar: result.avatar,
|
||||||
|
avatarStatic: result.avatar_static,
|
||||||
|
avatarDescription: result.avatar_description,
|
||||||
|
header: result.header,
|
||||||
|
headerStatic: result.header_static,
|
||||||
|
headerDescription: result.header_description,
|
||||||
|
locked: result.locked,
|
||||||
|
bot: result.bot,
|
||||||
|
hideCollections: result.hide_collections,
|
||||||
|
discoverable: result.discoverable,
|
||||||
|
indexable: result.indexable,
|
||||||
|
showMedia: result.show_media,
|
||||||
|
showMediaReplies: result.show_media_replies,
|
||||||
|
showFeatured: result.show_featured,
|
||||||
|
attributionDomains: result.attribution_domains,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchProfile = createDataLoadingThunk(
|
||||||
|
`${profileEditSlice.name}/fetchProfile`,
|
||||||
|
apiGetProfile,
|
||||||
|
transformProfile,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const patchProfile = createDataLoadingThunk(
|
||||||
|
`${profileEditSlice.name}/patchProfile`,
|
||||||
|
(params: Partial<ApiProfileUpdateParams>) => apiPatchProfile(params),
|
||||||
|
transformProfile,
|
||||||
|
{ useLoadingBar: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const fetchFeaturedTags = createDataLoadingThunk(
|
export const fetchFeaturedTags = createDataLoadingThunk(
|
||||||
`${profileEditSlice.name}/fetchFeaturedTags`,
|
`${profileEditSlice.name}/fetchFeaturedTags`,
|
||||||
apiGetCurrentFeaturedTags,
|
apiGetCurrentFeaturedTags,
|
||||||
@@ -143,7 +214,10 @@ export const addFeaturedTag = createDataLoadingThunk(
|
|||||||
{
|
{
|
||||||
condition(arg, { getState }) {
|
condition(arg, { getState }) {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
return !state.profileEdit.tags.some((tag) => tag.name === arg.name);
|
return (
|
||||||
|
!!state.profileEdit.tags &&
|
||||||
|
!state.profileEdit.tags.some((tag) => tag.name === arg.name)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,3 +24,8 @@ export type OmitValueType<T, V> = {
|
|||||||
export type AnyFunction = (...args: never) => unknown;
|
export type AnyFunction = (...args: never) => unknown;
|
||||||
|
|
||||||
export type OmitUnion<TUnion, TBase> = TBase & Omit<TUnion, keyof TBase>;
|
export type OmitUnion<TUnion, TBase> = TBase & Omit<TUnion, keyof TBase>;
|
||||||
|
|
||||||
|
export type SnakeToCamelCase<S extends string> =
|
||||||
|
S extends `${infer T}_${infer U}`
|
||||||
|
? `${T}${Capitalize<SnakeToCamelCase<U>>}`
|
||||||
|
: S;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
class REST::ProfileSerializer < ActiveModel::Serializer
|
class REST::ProfileSerializer < ActiveModel::Serializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
# Please update app/javascript/api_types/profile.ts when making changes to the attributes
|
||||||
attributes :id, :display_name, :note, :fields,
|
attributes :id, :display_name, :note, :fields,
|
||||||
:avatar, :avatar_static, :avatar_description, :header, :header_static, :header_description,
|
:avatar, :avatar_static, :avatar_description, :header, :header_static, :header_description,
|
||||||
:locked, :bot,
|
:locked, :bot,
|
||||||
|
|||||||
Reference in New Issue
Block a user