From b0a68a2867b2450f67427445a9c068443047a492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 21 Feb 2026 00:01:05 +0100 Subject: [PATCH] Nicolium: subscribers page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../features/ui/components/profile-stats.tsx | 28 +++--- .../pl-fe/src/features/ui/router/index.tsx | 11 +++ .../src/features/ui/util/async-components.ts | 1 + packages/pl-fe/src/locales/en.json | 3 + .../src/pages/account-lists/subscribers.tsx | 97 +++++++++++++++++++ .../src/queries/account-lists/use-follows.ts | 14 ++- 6 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 packages/pl-fe/src/pages/account-lists/subscribers.tsx diff --git a/packages/pl-fe/src/features/ui/components/profile-stats.tsx b/packages/pl-fe/src/features/ui/components/profile-stats.tsx index 688d25d43..525034424 100644 --- a/packages/pl-fe/src/features/ui/components/profile-stats.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-stats.tsx @@ -80,18 +80,24 @@ const ProfileStats: React.FC = ({ account, onClickHandler }) => { {account.subscribers_count > 0 && ( - // - - {!demetricator && ( - - {shortNumberFormat(account.subscribers_count)} + + + {!demetricator && ( + + {shortNumberFormat(account.subscribers_count)} + + )} + + - )} - - - - - // + + )} ); diff --git a/packages/pl-fe/src/features/ui/router/index.tsx b/packages/pl-fe/src/features/ui/router/index.tsx index 7c6e1029e..14767d3df 100644 --- a/packages/pl-fe/src/features/ui/router/index.tsx +++ b/packages/pl-fe/src/features/ui/router/index.tsx @@ -142,6 +142,7 @@ import { SettingsStore, Share, Status, + Subscribers, TestTimeline, ThemeEditor, Privacy, @@ -729,6 +730,15 @@ export const profileFollowingRoute = createRoute({ component: Following, }); +export const profileSubscribersRoute = createRoute({ + getParentRoute: () => layouts.profile, + path: '/subscribers', + component: Subscribers, + validateSearch: v.object({ + include_expired: v.optional(v.boolean(), false), + }), +}); + export const profileMediaRoute = createRoute({ getParentRoute: () => layouts.profile, path: '/media', @@ -1538,6 +1548,7 @@ const routeTree = rootRoute.addChildren([ profileRoute, profileFollowersRoute, profileFollowingRoute, + profileSubscribersRoute, profileMediaRoute, profileTaggedRoute, profileFavoritesRoute, diff --git a/packages/pl-fe/src/features/ui/util/async-components.ts b/packages/pl-fe/src/features/ui/util/async-components.ts index 0bfd2e8ae..8a7db9e6a 100644 --- a/packages/pl-fe/src/features/ui/util/async-components.ts +++ b/packages/pl-fe/src/features/ui/util/async-components.ts @@ -106,6 +106,7 @@ export const Settings = lazy(() => import('@/pages/settings/settings')); export const SettingsStore = lazy(() => import('@/pages/developers/settings-store')); export const Share = lazy(() => import('@/pages/utils/share')); export const Status = lazy(() => import('@/pages/statuses/status')); +export const Subscribers = lazy(() => import('@/pages/account-lists/subscribers')); export const TestTimeline = lazy(() => import('@/pages/timelines/test-timeline')); export const ThemeEditor = lazy(() => import('@/pages/dashboard/theme-editor')); export const Privacy = lazy(() => import('@/pages/settings/privacy')); diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index b9abdb2d1..2eb067024 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -74,6 +74,8 @@ "account.subscribe.failure": "An error occurred trying to subscribe to this account.", "account.subscribe.success": "You have subscribed to this account.", "account.subscribers": "Subscribers", + "account.subscribers.empty": "No one subscribes to this user yet.", + "account.subscribers.include_expired": "Include expired subscriptions", "account.timezone": "Timezone: {timezone}", "account.timezone.equal": "(same as you)", "account.unblock": "Unblock @{name}", @@ -508,6 +510,7 @@ "column.scheduled_statuses": "Scheduled posts", "column.search": "Search", "column.settings_store": "Settings store", + "column.subscribers": "Subscribers", "column.test": "Test timeline", "column.tokens": "Active sessions", "column.wrenched": "Recent wrenches timeline", diff --git a/packages/pl-fe/src/pages/account-lists/subscribers.tsx b/packages/pl-fe/src/pages/account-lists/subscribers.tsx new file mode 100644 index 000000000..a7f285f7b --- /dev/null +++ b/packages/pl-fe/src/pages/account-lists/subscribers.tsx @@ -0,0 +1,97 @@ +import { useNavigate } from '@tanstack/react-router'; +import React from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; +import List, { ListItem } from '@/components/list'; +import MissingIndicator from '@/components/missing-indicator'; +import ScrollableList from '@/components/scrollable-list'; +import Column from '@/components/ui/column'; +import Spinner from '@/components/ui/spinner'; +import Toggle from '@/components/ui/toggle'; +import AccountContainer from '@/containers/account-container'; +import { profileSubscribersRoute } from '@/features/ui/router'; +import { useSubscribers } from '@/queries/account-lists/use-follows'; + +const messages = defineMessages({ + heading: { id: 'column.subscribers', defaultMessage: 'Subscribers' }, +}); + +/** Displays a list of accounts subscribing the given account. */ +const SubscribersPage: React.FC = () => { + const navigate = useNavigate({ from: profileSubscribersRoute.fullPath }); + const { username } = profileSubscribersRoute.useParams(); + const { include_expired: includeExpired } = profileSubscribersRoute.useSearch(); + + const intl = useIntl(); + + const { account, isUnavailable } = useAccountLookup(username); + + const { + data = [], + hasNextPage, + fetchNextPage, + isFetching, + isLoading, + } = useSubscribers(account?.id, includeExpired); + + if (isLoading) { + return ; + } + + if (!account) { + return ; + } + + if (isUnavailable) { + return ( +
+ +
+ ); + } + + return ( + + + + } + > + navigate({ search: { include_expired: !includeExpired } })} + /> + + + + + } + itemClassName='pb-4' + isLoading={isFetching} + > + {data.map((accountId) => ( + + ))} + + + ); +}; + +export { SubscribersPage as default }; diff --git a/packages/pl-fe/src/queries/account-lists/use-follows.ts b/packages/pl-fe/src/queries/account-lists/use-follows.ts index 6afac8d81..1502553f9 100644 --- a/packages/pl-fe/src/queries/account-lists/use-follows.ts +++ b/packages/pl-fe/src/queries/account-lists/use-follows.ts @@ -22,10 +22,18 @@ const useFollowing = makePaginatedResponseQuery( ); const useSubscribers = makePaginatedResponseQuery( - (accountId?: string) => ['accountsLists', 'subscribers', accountId], - (client, [accountId]) => + (accountId?: string, includeExpired?: boolean) => [ + 'accountsLists', + 'subscribers', + accountId, + includeExpired || false, + ], + (client, [accountId, includeExpired]) => client.accounts - .getAccountSubscribers(accountId!, { with_relationships: true }) + .getAccountSubscribers(accountId!, { + include_expired: includeExpired, + with_relationships: true, + }) .then(minifyAccountList), undefined, (accountId) => !!accountId,