Nicolium: subscribers page

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-02-21 00:01:05 +01:00
parent ef2834b132
commit b0a68a2867
6 changed files with 140 additions and 14 deletions

View File

@@ -80,18 +80,24 @@ const ProfileStats: React.FC<IProfileStats> = ({ account, onClickHandler }) => {
</Link>
{account.subscribers_count > 0 && (
// <Link to='/@{$username}/subscribers' params={{ username: account.acct }} onClick={onClickHandler} title={intl.formatNumber(account.subscribers_count)} className='hover:underline'>
<HStack alignItems='center' space={1}>
{!demetricator && (
<Text theme='primary' weight='bold' size='sm'>
{shortNumberFormat(account.subscribers_count)}
<Link
to='/@{$username}/subscribers'
params={{ username: account.acct }}
onClick={onClickHandler}
title={intl.formatNumber(account.subscribers_count)}
className='hover:underline'
>
<HStack alignItems='center' space={1}>
{!demetricator && (
<Text theme='primary' weight='bold' size='sm'>
{shortNumberFormat(account.subscribers_count)}
</Text>
)}
<Text weight='bold' size='sm'>
<FormattedMessage id='account.subscribers' defaultMessage='Subscribers' />
</Text>
)}
<Text weight='bold' size='sm'>
<FormattedMessage id='account.subscribers' defaultMessage='Subscribers' />
</Text>
</HStack>
// </Link>
</HStack>
</Link>
)}
</HStack>
);

View File

@@ -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,

View File

@@ -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'));

View File

@@ -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",

View File

@@ -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 <Spinner />;
}
if (!account) {
return <MissingIndicator />;
}
if (isUnavailable) {
return (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>
</div>
);
}
return (
<Column label={intl.formatMessage(messages.heading)} transparent>
<List>
<ListItem
className='mb-3 black:mx-4 black:mb-0'
label={
<FormattedMessage
id='account.subscribers.include_expired'
defaultMessage='Include expired subscriptions'
/>
}
>
<Toggle
checked={includeExpired}
onChange={() => navigate({ search: { include_expired: !includeExpired } })}
/>
</ListItem>
</List>
<ScrollableList
scrollKey='subscribers'
hasMore={hasNextPage}
onLoadMore={fetchNextPage}
emptyMessageText={
<FormattedMessage
id='account.subscribers.empty'
defaultMessage='No one subscribes to this user yet.'
/>
}
itemClassName='pb-4'
isLoading={isFetching}
>
{data.map((accountId) => (
<AccountContainer key={accountId} id={accountId} />
))}
</ScrollableList>
</Column>
);
};
export { SubscribersPage as default };

View File

@@ -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,