fix: Update hashtags when (un)following a hashtag (#35101)

This commit is contained in:
diondiondion 2025-06-23 13:44:59 +02:00 committed by GitHub
commit b9b1500fc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 107 additions and 65 deletions

View file

@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useEffect, useCallback, useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@ -7,8 +7,10 @@ import { Helmet } from 'react-helmet';
import { isFulfilled } from '@reduxjs/toolkit';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { unfollowHashtag } from 'mastodon/actions/tags_typed';
import { apiGetFollowedTags } from 'mastodon/api/tags';
import {
fetchFollowedHashtags,
unfollowHashtag,
} from 'mastodon/actions/tags_typed';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
@ -16,7 +18,7 @@ import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Hashtag } from 'mastodon/components/hashtag';
import ScrollableList from 'mastodon/components/scrollable_list';
import { useAppDispatch } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
@ -59,55 +61,32 @@ const FollowedTag: React.FC<{
const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
const [loading, setLoading] = useState(false);
const [next, setNext] = useState<string | undefined>();
const dispatch = useAppDispatch();
const { tags, loading, next, stale } = useAppSelector(
(state) => state.followedTags,
);
const hasMore = !!next;
const columnRef = useRef<ColumnRef>(null);
useEffect(() => {
setLoading(true);
void apiGetFollowedTags()
.then(({ tags, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
setTags(tags);
setLoading(false);
setNext(next?.uri);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setTags, setLoading, setNext]);
if (stale) {
void dispatch(fetchFollowedHashtags());
}
}, [dispatch, stale]);
const handleLoadMore = useCallback(() => {
setLoading(true);
void apiGetFollowedTags(next)
.then(({ tags, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
setLoading(false);
setTags((previousTags) => [...previousTags, ...tags]);
setNext(next?.uri);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setTags, setLoading, setNext, next]);
if (next) {
void dispatch(fetchFollowedHashtags({ next }));
}
}, [dispatch, next]);
const handleUnfollow = useCallback(
(tagId: string) => {
setTags((tags) => tags.filter((tag) => tag.name !== tagId));
void dispatch(unfollowHashtag({ tagId }));
},
[setTags],
[dispatch],
);
const columnRef = useRef<ColumnRef>(null);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);

View file

@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { apiGetFollowedTags } from 'mastodon/api/tags';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { fetchFollowedHashtags } from 'mastodon/actions/tags_typed';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { CollapsiblePanel } from './collapsible_panel';
@ -24,25 +24,20 @@ const messages = defineMessages({
},
});
const TAG_LIMIT = 4;
export const FollowedTagsPanel: React.FC = () => {
const intl = useIntl();
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const { tags, stale, loading } = useAppSelector(
(state) => state.followedTags,
);
useEffect(() => {
setLoading(true);
void apiGetFollowedTags(undefined, 4)
.then(({ tags }) => {
setTags(tags);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setLoading, setTags]);
if (stale) {
void dispatch(fetchFollowedHashtags());
}
}, [dispatch, stale]);
return (
<CollapsiblePanel
@ -54,14 +49,14 @@ export const FollowedTagsPanel: React.FC = () => {
expandTitle={intl.formatMessage(messages.expand)}
loading={loading}
>
{tags.map((tag) => (
{tags.slice(0, TAG_LIMIT).map((tag) => (
<ColumnLink
transparent
icon='hashtag'
key={tag.name}
iconComponent={TagIcon}
text={`#${tag.name}`}
to={`/tags/${tag.name}`}
transparent
/>
))}
</CollapsiblePanel>

View file

@ -16,7 +16,6 @@ export const ColumnLink: React.FC<{
method?: string;
badge?: React.ReactNode;
transparent?: boolean;
optional?: boolean;
className?: string;
id?: string;
}> = ({
@ -30,13 +29,11 @@ export const ColumnLink: React.FC<{
method,
badge,
transparent,
optional,
...other
}) => {
const match = useRouteMatch(to ?? '');
const className = classNames('column-link', {
'column-link--transparent': transparent,
'column-link--optional': optional,
});
const badgeElement =
typeof badge !== 'undefined' ? (