From 40157e063d12b5591a9cccaf92abd783e03586b4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 28 Apr 2025 13:44:01 +0200 Subject: [PATCH] Add ability to feature and unfeature hashtags from web UI (#34490) --- app/javascript/mastodon/actions/tags_typed.ts | 18 +++++- app/javascript/mastodon/api/tags.ts | 6 ++ app/javascript/mastodon/api_types/tags.ts | 1 + .../components/hashtag_header.tsx | 60 +++++++++++++++---- app/javascript/mastodon/locales/en.json | 2 + 5 files changed, 74 insertions(+), 13 deletions(-) diff --git a/app/javascript/mastodon/actions/tags_typed.ts b/app/javascript/mastodon/actions/tags_typed.ts index 6dca32fd84..a3e5cfd125 100644 --- a/app/javascript/mastodon/actions/tags_typed.ts +++ b/app/javascript/mastodon/actions/tags_typed.ts @@ -1,4 +1,10 @@ -import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags'; +import { + apiGetTag, + apiFollowTag, + apiUnfollowTag, + apiFeatureTag, + apiUnfeatureTag, +} from 'mastodon/api/tags'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; export const fetchHashtag = createDataLoadingThunk( @@ -15,3 +21,13 @@ export const unfollowHashtag = createDataLoadingThunk( 'tags/unfollow', ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId), ); + +export const featureHashtag = createDataLoadingThunk( + 'tags/feature', + ({ tagId }: { tagId: string }) => apiFeatureTag(tagId), +); + +export const unfeatureHashtag = createDataLoadingThunk( + 'tags/unfeature', + ({ tagId }: { tagId: string }) => apiUnfeatureTag(tagId), +); diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts index 4b111def81..cb84ccb1c4 100644 --- a/app/javascript/mastodon/api/tags.ts +++ b/app/javascript/mastodon/api/tags.ts @@ -10,6 +10,12 @@ export const apiFollowTag = (tagId: string) => export const apiUnfollowTag = (tagId: string) => apiRequestPost(`v1/tags/${tagId}/unfollow`); +export const apiFeatureTag = (tagId: string) => + apiRequestPost(`v1/tags/${tagId}/feature`); + +export const apiUnfeatureTag = (tagId: string) => + apiRequestPost(`v1/tags/${tagId}/unfeature`); + export const apiGetFollowedTags = async (url?: string) => { const response = await api().request({ method: 'GET', diff --git a/app/javascript/mastodon/api_types/tags.ts b/app/javascript/mastodon/api_types/tags.ts index 0c16c8bd28..3066b4f1f1 100644 --- a/app/javascript/mastodon/api_types/tags.ts +++ b/app/javascript/mastodon/api_types/tags.ts @@ -10,4 +10,5 @@ export interface ApiHashtagJSON { url: string; history: [ApiHistoryJSON, ...ApiHistoryJSON[]]; following?: boolean; + featuring?: boolean; } diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx index b7c1ea02d9..c11874e7d4 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx +++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx @@ -9,6 +9,8 @@ import { fetchHashtag, followHashtag, unfollowHashtag, + featureHashtag, + unfeatureHashtag, } from 'mastodon/actions/tags_typed'; import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; import { Button } from 'mastodon/components/button'; @@ -28,6 +30,11 @@ const messages = defineMessages({ id: 'hashtag.admin_moderation', defaultMessage: 'Open moderation interface for #{name}', }, + feature: { id: 'hashtag.feature', defaultMessage: 'Feature on profile' }, + unfeature: { + id: 'hashtag.unfeature', + defaultMessage: "Don't feature on profile", + }, }); const usesRenderer = (displayNumber: React.ReactNode, pluralReady: number) => ( @@ -88,22 +95,51 @@ export const HashtagHeader: React.FC<{ }, [dispatch, tagId, setTag]); const menu = useMemo(() => { - const tmp = []; + const arr = []; - if ( - tag && - signedIn && - (permissions & PERMISSION_MANAGE_TAXONOMIES) === - PERMISSION_MANAGE_TAXONOMIES - ) { - tmp.push({ - text: intl.formatMessage(messages.adminModeration, { name: tag.id }), - href: `/admin/tags/${tag.id}`, + if (tag && signedIn) { + const handleFeature = () => { + if (tag.featuring) { + void dispatch(unfeatureHashtag({ tagId })).then((result) => { + if (isFulfilled(result)) { + setTag(result.payload); + } + + return ''; + }); + } else { + void dispatch(featureHashtag({ tagId })).then((result) => { + if (isFulfilled(result)) { + setTag(result.payload); + } + + return ''; + }); + } + }; + + arr.push({ + text: intl.formatMessage( + tag.featuring ? messages.unfeature : messages.feature, + ), + action: handleFeature, }); + + arr.push(null); + + if ( + (permissions & PERMISSION_MANAGE_TAXONOMIES) === + PERMISSION_MANAGE_TAXONOMIES + ) { + arr.push({ + text: intl.formatMessage(messages.adminModeration, { name: tagId }), + href: `/admin/tags/${tag.id}`, + }); + } } - return tmp; - }, [signedIn, permissions, intl, tag]); + return arr; + }, [setTag, dispatch, tagId, signedIn, permissions, intl, tag]); const handleFollow = useCallback(() => { if (!signedIn || !tag) { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 6b4a0a12ab..e193e10a6d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -405,8 +405,10 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today", + "hashtag.feature": "Feature on profile", "hashtag.follow": "Follow hashtag", "hashtag.mute": "Mute #{hashtag}", + "hashtag.unfeature": "Don't feature on profile", "hashtag.unfollow": "Unfollow hashtag", "hashtags.and_other": "…and {count, plural, other {# more}}", "hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",