Profile editing: Allow adding arbitrary featured tags (#38012)

This commit is contained in:
Echo authored and GitHub committed 2026-03-02 16:32:08 +00:00
1 parent 03b2f77ad2
commit 74b3b6c798
8 files changed
+120 -65

No files matched your search

@@ -1,9 +1,9 @@
import type { ChangeEventHandler, FC } from 'react';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
import type { ApiHashtagJSON } from '@/mastodon/api_types/tags';
import { Combobox } from '@/mastodon/components/form_fields';
import {
addFeaturedTag,
@@ -15,10 +15,50 @@ import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import classes from '../styles.module.scss';
type SearchResult = Omit<ApiHashtagJSON, 'url' | 'history'> & {
label?: string;
};
const messages = defineMessages({
placeholder: {
id: 'account_edit_tags.search_placeholder',
defaultMessage: 'Enter a hashtag…',
},
addTag: {
id: 'account_edit_tags.add_tag',
defaultMessage: 'Add #{tagName}',
},
});
export const AccountEditTagSearch: FC = () => {
const { query, isLoading, results } = useAppSelector(
(state) => state.profileEdit.search,
);
const intl = useIntl();
const {
query,
isLoading,
results: rawResults,
} = useAppSelector((state) => state.profileEdit.search);
const results = useMemo(() => {
if (!rawResults) {
return [];
}
const results: SearchResult[] = [...rawResults]; // Make array mutable
const trimmedQuery = query.trim();
if (
trimmedQuery.length > 0 &&
results.every(
(result) => result.name.toLowerCase() !== trimmedQuery.toLowerCase(),
)
) {
results.push({
id: 'new',
name: trimmedQuery,
label: intl.formatMessage(messages.addTag, { tagName: trimmedQuery }),
});
}
return results;
}, [intl, query, rawResults]);
const dispatch = useAppDispatch();
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useCallback(
@@ -28,10 +68,8 @@ export const AccountEditTagSearch: FC = () => {
[dispatch],
);
const intl = useIntl();
const handleSelect = useCallback(
(item: ApiFeaturedTagJSON) => {
(item: SearchResult) => {
void dispatch(clearSearch());
void dispatch(addFeaturedTag({ name: item.name }));
},
@@ -42,11 +80,8 @@ export const AccountEditTagSearch: FC = () => {
<Combobox
value={query}
onChange={handleSearchChange}
placeholder={intl.formatMessage({
id: 'account_edit_tags.search_placeholder',
defaultMessage: 'Enter a hashtag…',
})}
items={results ?? []}
placeholder={intl.formatMessage(messages.placeholder)}
items={results}
isLoading={isLoading}
renderItem={renderItem}
onSelectItem={handleSelect}
@@ -57,4 +92,4 @@ export const AccountEditTagSearch: FC = () => {
);
};
const renderItem = (item: ApiFeaturedTagJSON) => <p>#{item.name}</p>;
const renderItem = (item: SearchResult) => item.label ?? `#${item.name}`;
@@ -3,15 +3,15 @@ import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { Tag } from '@/mastodon/components/tags/tag';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import type { TagData } from '@/mastodon/reducers/slices/profile_edit';
import {
addFeaturedTag,
deleteFeaturedTag,
fetchFeaturedTags,
fetchProfile,
fetchSuggestedTags,
} from '@/mastodon/reducers/slices/profile_edit';
import {
@@ -35,9 +35,9 @@ const messages = defineMessages({
const selectTags = createAppSelector(
[(state) => state.profileEdit],
(profileEdit) => ({
tags: profileEdit.tags ?? [],
tags: profileEdit.profile?.featuredTags ?? [],
tagSuggestions: profileEdit.tagSuggestions ?? [],
isLoading: !profileEdit.tags || !profileEdit.tagSuggestions,
isLoading: !profileEdit.profile || !profileEdit.tagSuggestions,
isPending: profileEdit.isPending,
}),
);
@@ -52,7 +52,7 @@ export const AccountEditFeaturedTags: FC = () => {
const dispatch = useAppDispatch();
useEffect(() => {
void dispatch(fetchFeaturedTags());
void dispatch(fetchProfile());
void dispatch(fetchSuggestedTags());
}, [dispatch]);
@@ -78,7 +78,9 @@ export const AccountEditFeaturedTags: FC = () => {
defaultMessage='Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile page’s Activity view.'
tagName='p'
/>
<AccountEditTagSearch />
{tagSuggestions.length > 0 && (
<div className={classes.tagSuggestions}>
<FormattedMessage
@@ -90,7 +92,9 @@ export const AccountEditFeaturedTags: FC = () => {
))}
</div>
)}
{isLoading && <LoadingIndicator />}
<AccountEditItemList
items={tags}
disabled={isPending}
@@ -102,15 +106,15 @@ export const AccountEditFeaturedTags: FC = () => {
);
};
function renderTag(tag: ApiFeaturedTagJSON) {
function renderTag(tag: TagData) {
return (
<div className={classes.tagItem}>
<h4>#{tag.name}</h4>
{tag.statuses_count > 0 && (
{tag.statusesCount > 0 && (
<FormattedMessage
id='account_edit_tags.tag_status_count'
defaultMessage='{count, plural, one {# post} other {# posts}}'
values={{ count: tag.statuses_count }}
values={{ count: tag.statusesCount }}
tagName='p'
/>
)}
@@ -15,10 +15,7 @@ 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,
fetchProfile,
} from '@/mastodon/reducers/slices/profile_edit';
import { fetchProfile } from '@/mastodon/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
@@ -87,9 +84,8 @@ export const AccountEdit: FC = () => {
const dispatch = useAppDispatch();
const { profile, tags = [] } = useAppSelector((state) => state.profileEdit);
const { profile } = useAppSelector((state) => state.profileEdit);
useEffect(() => {
void dispatch(fetchFeaturedTags());
void dispatch(fetchProfile());
}, [dispatch]);
@@ -127,7 +123,7 @@ export const AccountEdit: FC = () => {
const headerSrc = autoPlayGif ? profile.header : profile.headerStatic;
const hasName = !!profile.displayName;
const hasBio = !!profile.bio;
const hasTags = tags.length > 0;
const hasTags = profile.featuredTags.length > 0;
return (
<AccountEditColumn
@@ -190,7 +186,7 @@ export const AccountEdit: FC = () => {
/>
}
>
{tags.map((tag) => `#${tag.name}`).join(', ')}
{profile.featuredTags.map((tag) => `#${tag.name}`).join(', ')}
</AccountEditSection>
<AccountEditSection