Profile editing: Utilize new API (#37990)
This commit is contained in:
@@ -179,3 +179,10 @@ export async function apiRequestDelete<
|
||||
>(url: ApiUrl, params?: RequestParamsOrData<ApiParams>) {
|
||||
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 {
|
||||
ApiAccountJSON,
|
||||
ApiFamiliarFollowersJSON,
|
||||
@@ -9,6 +14,11 @@ import type {
|
||||
ApiHashtagJSON,
|
||||
} from 'mastodon/api_types/tags';
|
||||
|
||||
import type {
|
||||
ApiProfileJSON,
|
||||
ApiProfileUpdateParams,
|
||||
} from '../api_types/profile';
|
||||
|
||||
export const apiSubmitAccountNote = (id: string, value: string) =>
|
||||
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
@@ -54,3 +64,8 @@ export const apiGetFamiliarFollowers = (id: string) =>
|
||||
apiRequestGet<ApiFamiliarFollowersJSON>('v1/accounts/familiar_followers', {
|
||||
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 { TextArea } from '@/mastodon/components/form_fields';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import classes from '../styles.module.scss';
|
||||
|
||||
@@ -38,10 +37,11 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
||||
const titleId = useId();
|
||||
const counterId = useId();
|
||||
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(
|
||||
(event) => {
|
||||
setNewBio(event.currentTarget.value);
|
||||
@@ -55,19 +55,22 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!account) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
const dispatch = useAppDispatch();
|
||||
const handleSave = useCallback(() => {
|
||||
if (!isPending) {
|
||||
void dispatch(patchProfile({ note: newBio })).then(onClose);
|
||||
}
|
||||
}, [dispatch, isPending, newBio, onClose]);
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={intl.formatMessage(
|
||||
account.note_plain ? messages.editTitle : messages.addTitle,
|
||||
)}
|
||||
title={intl.formatMessage(bio ? messages.editTitle : messages.addTitle)}
|
||||
titleId={titleId}
|
||||
confirm={intl.formatMessage(messages.save)}
|
||||
onConfirm={onClose} // To be implemented
|
||||
onConfirm={handleSave}
|
||||
onClose={onClose}
|
||||
updating={isPending}
|
||||
disabled={newBio.length > MAX_BIO_LENGTH}
|
||||
noFocusButton
|
||||
>
|
||||
<div className={classes.inputWrapper}>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { TextInput } from '@/mastodon/components/form_fields';
|
||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import classes from '../styles.module.scss';
|
||||
|
||||
@@ -37,10 +37,11 @@ export const NameModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
|
||||
const titleId = useId();
|
||||
const counterId = useId();
|
||||
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(
|
||||
(event) => {
|
||||
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 (
|
||||
<ConfirmationModal
|
||||
title={intl.formatMessage(messages.editTitle)}
|
||||
titleId={titleId}
|
||||
confirm={intl.formatMessage(messages.save)}
|
||||
onConfirm={onClose} // To be implemented
|
||||
onConfirm={handleSave}
|
||||
onClose={onClose}
|
||||
updating={isPending}
|
||||
disabled={newName.length > MAX_NAME_LENGTH}
|
||||
noCloseOnConfirm
|
||||
noFocusButton
|
||||
>
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
fetchFeaturedTags,
|
||||
fetchSuggestedTags,
|
||||
} 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 { 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 = () => {
|
||||
const accountId = useCurrentAccountId();
|
||||
const account = useAccount(accountId);
|
||||
const intl = useIntl();
|
||||
|
||||
const { tags, tagSuggestions, isLoading, isPending } = useAppSelector(
|
||||
(state) => state.profileEdit,
|
||||
);
|
||||
const { tags, tagSuggestions, isLoading, isPending } =
|
||||
useAppSelector(selectTags);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,13 +7,17 @@ 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 { 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 { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
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 { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
||||
@@ -82,11 +86,10 @@ export const AccountEdit: FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { tags: featuredTags, isLoading: isTagsLoading } = useAppSelector(
|
||||
(state) => state.profileEdit,
|
||||
);
|
||||
const { profile, tags = [] } = useAppSelector((state) => state.profileEdit);
|
||||
useEffect(() => {
|
||||
void dispatch(fetchFeaturedTags());
|
||||
void dispatch(fetchProfile());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleOpenModal = useCallback(
|
||||
@@ -107,14 +110,20 @@ export const AccountEdit: FC = () => {
|
||||
history.push('/profile/featured_tags');
|
||||
}, [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} />;
|
||||
}
|
||||
|
||||
const headerSrc = autoPlayGif ? account.header : account.header_static;
|
||||
const hasName = !!account.display_name;
|
||||
const hasBio = !!account.note_plain;
|
||||
const hasTags = !isTagsLoading && featuredTags.length > 0;
|
||||
const headerSrc = autoPlayGif ? profile.header : profile.headerStatic;
|
||||
const hasName = !!profile.displayName;
|
||||
const hasBio = !!profile.bio;
|
||||
const hasTags = tags.length > 0;
|
||||
|
||||
return (
|
||||
<AccountEditColumn
|
||||
@@ -128,62 +137,64 @@ export const AccountEdit: FC = () => {
|
||||
<Avatar account={account} size={80} className={classes.avatar} />
|
||||
</header>
|
||||
|
||||
<AccountEditSection
|
||||
title={messages.displayNameTitle}
|
||||
description={messages.displayNamePlaceholder}
|
||||
showDescription={!hasName}
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleNameEdit}
|
||||
item={messages.displayNameTitle}
|
||||
edit={hasName}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DisplayNameSimple account={account} />
|
||||
</AccountEditSection>
|
||||
<CustomEmojiProvider emojis={emojis}>
|
||||
<AccountEditSection
|
||||
title={messages.displayNameTitle}
|
||||
description={messages.displayNamePlaceholder}
|
||||
showDescription={!hasName}
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleNameEdit}
|
||||
item={messages.displayNameTitle}
|
||||
edit={hasName}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EmojiHTML htmlString={profile.displayName} {...htmlHandlers} />
|
||||
</AccountEditSection>
|
||||
|
||||
<AccountEditSection
|
||||
title={messages.bioTitle}
|
||||
description={messages.bioPlaceholder}
|
||||
showDescription={!hasBio}
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleBioEdit}
|
||||
item={messages.bioTitle}
|
||||
edit={hasBio}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AccountBio accountId={accountId} />
|
||||
</AccountEditSection>
|
||||
<AccountEditSection
|
||||
title={messages.bioTitle}
|
||||
description={messages.bioPlaceholder}
|
||||
showDescription={!hasBio}
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleBioEdit}
|
||||
item={messages.bioTitle}
|
||||
edit={hasBio}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EmojiHTML htmlString={profile.bio} {...htmlHandlers} />
|
||||
</AccountEditSection>
|
||||
|
||||
<AccountEditSection
|
||||
title={messages.customFieldsTitle}
|
||||
description={messages.customFieldsPlaceholder}
|
||||
showDescription
|
||||
/>
|
||||
<AccountEditSection
|
||||
title={messages.customFieldsTitle}
|
||||
description={messages.customFieldsPlaceholder}
|
||||
showDescription
|
||||
/>
|
||||
|
||||
<AccountEditSection
|
||||
title={messages.featuredHashtagsTitle}
|
||||
description={messages.featuredHashtagsPlaceholder}
|
||||
showDescription={!hasTags}
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleFeaturedTagsEdit}
|
||||
edit={hasTags}
|
||||
item={messages.featuredHashtagsItem}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{featuredTags.map((tag) => `#${tag.name}`).join(', ')}
|
||||
</AccountEditSection>
|
||||
<AccountEditSection
|
||||
title={messages.featuredHashtagsTitle}
|
||||
description={messages.featuredHashtagsPlaceholder}
|
||||
showDescription={!hasTags}
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleFeaturedTagsEdit}
|
||||
edit={hasTags}
|
||||
item={messages.featuredHashtagsItem}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{tags.map((tag) => `#${tag.name}`).join(', ')}
|
||||
</AccountEditSection>
|
||||
|
||||
<AccountEditSection
|
||||
title={messages.profileTabTitle}
|
||||
description={messages.profileTabSubtitle}
|
||||
showDescription
|
||||
/>
|
||||
<AccountEditSection
|
||||
title={messages.profileTabTitle}
|
||||
description={messages.profileTabSubtitle}
|
||||
showDescription
|
||||
/>
|
||||
</CustomEmojiProvider>
|
||||
</AccountEditColumn>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
|
||||
import { buildCustomEmojis } from '../features/emoji/emoji';
|
||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
||||
|
||||
/** @type {ImmutableList<import('@/mastodon/models/custom_emoji').CustomEmoji>} */
|
||||
const initialState = ImmutableList([]);
|
||||
|
||||
export default function custom_emojis(state = initialState, action) {
|
||||
|
||||
@@ -6,10 +6,16 @@ import { debounce } from 'lodash';
|
||||
import {
|
||||
apiDeleteFeaturedTag,
|
||||
apiGetCurrentFeaturedTags,
|
||||
apiGetProfile,
|
||||
apiGetTagSuggestions,
|
||||
apiPatchProfile,
|
||||
apiPostFeaturedTag,
|
||||
} from '@/mastodon/api/accounts';
|
||||
import { apiGetSearch } from '@/mastodon/api/search';
|
||||
import type {
|
||||
ApiProfileJSON,
|
||||
ApiProfileUpdateParams,
|
||||
} from '@/mastodon/api_types/profile';
|
||||
import { hashtagToFeaturedTag } from '@/mastodon/api_types/tags';
|
||||
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
|
||||
import type { AppDispatch } from '@/mastodon/store';
|
||||
@@ -17,11 +23,21 @@ import {
|
||||
createAppAsyncThunk,
|
||||
createDataLoadingThunk,
|
||||
} from '@/mastodon/store/typed_functions';
|
||||
import type { SnakeToCamelCase } from '@/mastodon/utils/types';
|
||||
|
||||
interface ProfileEditState {
|
||||
tags: ApiFeaturedTagJSON[];
|
||||
tagSuggestions: ApiFeaturedTagJSON[];
|
||||
isLoading: boolean;
|
||||
type ProfileData = {
|
||||
[Key in keyof Omit<
|
||||
ApiProfileJSON,
|
||||
'note'
|
||||
> as SnakeToCamelCase<Key>]: ApiProfileJSON[Key];
|
||||
} & {
|
||||
bio: ApiProfileJSON['note'];
|
||||
};
|
||||
|
||||
export interface ProfileEditState {
|
||||
profile?: ProfileData;
|
||||
tags?: ApiFeaturedTagJSON[];
|
||||
tagSuggestions?: ApiFeaturedTagJSON[];
|
||||
isPending: boolean;
|
||||
search: {
|
||||
query: string;
|
||||
@@ -31,9 +47,6 @@ interface ProfileEditState {
|
||||
}
|
||||
|
||||
const initialState: ProfileEditState = {
|
||||
tags: [],
|
||||
tagSuggestions: [],
|
||||
isLoading: true,
|
||||
isPending: false,
|
||||
search: {
|
||||
query: '',
|
||||
@@ -49,6 +62,7 @@ const profileEditSlice = createSlice({
|
||||
if (state.search.query === action.payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.search.query = action.payload;
|
||||
state.search.isLoading = false;
|
||||
state.search.results = undefined;
|
||||
@@ -60,13 +74,25 @@ const profileEditSlice = createSlice({
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(fetchProfile.fulfilled, (state, action) => {
|
||||
state.profile = action.payload;
|
||||
});
|
||||
builder.addCase(fetchSuggestedTags.fulfilled, (state, action) => {
|
||||
state.tagSuggestions = action.payload.map(hashtagToFeaturedTag);
|
||||
state.isLoading = false;
|
||||
});
|
||||
builder.addCase(fetchFeaturedTags.fulfilled, (state, action) => {
|
||||
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) => {
|
||||
@@ -76,12 +102,18 @@ const profileEditSlice = createSlice({
|
||||
state.isPending = false;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
||||
if (!state.tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.tags = [...state.tags, action.payload].toSorted(
|
||||
(a, b) => b.statuses_count - a.statuses_count,
|
||||
);
|
||||
state.tagSuggestions = state.tagSuggestions.filter(
|
||||
(tag) => tag.name !== action.meta.arg.name,
|
||||
);
|
||||
if (state.tagSuggestions) {
|
||||
state.tagSuggestions = state.tagSuggestions.filter(
|
||||
(tag) => tag.name !== action.meta.arg.name,
|
||||
);
|
||||
}
|
||||
state.isPending = false;
|
||||
});
|
||||
|
||||
@@ -92,6 +124,10 @@ const profileEditSlice = createSlice({
|
||||
state.isPending = false;
|
||||
});
|
||||
builder.addCase(deleteFeaturedTag.fulfilled, (state, action) => {
|
||||
if (!state.tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.tags = state.tags.filter((tag) => tag.id !== action.meta.arg.tagId);
|
||||
state.isPending = false;
|
||||
});
|
||||
@@ -106,7 +142,7 @@ const profileEditSlice = createSlice({
|
||||
builder.addCase(fetchSearchResults.fulfilled, (state, action) => {
|
||||
state.search.isLoading = false;
|
||||
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) {
|
||||
if (currentTags.has(tag.name)) {
|
||||
@@ -125,6 +161,41 @@ const profileEditSlice = createSlice({
|
||||
export const profileEdit = profileEditSlice.reducer;
|
||||
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(
|
||||
`${profileEditSlice.name}/fetchFeaturedTags`,
|
||||
apiGetCurrentFeaturedTags,
|
||||
@@ -143,7 +214,10 @@ export const addFeaturedTag = createDataLoadingThunk(
|
||||
{
|
||||
condition(arg, { 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 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
|
||||
include RoutingHelper
|
||||
|
||||
# Please update app/javascript/api_types/profile.ts when making changes to the attributes
|
||||
attributes :id, :display_name, :note, :fields,
|
||||
:avatar, :avatar_static, :avatar_description, :header, :header_static, :header_description,
|
||||
:locked, :bot,
|
||||
|
||||
Reference in New Issue
Block a user