Simplify/consolidate latest accounts code, fix suggestions pagination

merge-requests/3322/head
Alex Gleason 2025-02-01 22:05:51 -06:00
rodzic 742e339c53
commit 2d973c9cd0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
4 zmienionych plików z 51 dodań i 91 usunięć

Wyświetl plik

@ -1,41 +1,33 @@
import { debounce } from 'es-toolkit'; import { debounce } from 'es-toolkit';
import { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchSuggestions } from 'soapbox/actions/suggestions.ts';
import ScrollableList from 'soapbox/components/scrollable-list.tsx'; import ScrollableList from 'soapbox/components/scrollable-list.tsx';
import { Column } from 'soapbox/components/ui/column.tsx'; import { Column } from 'soapbox/components/ui/column.tsx';
import Stack from 'soapbox/components/ui/stack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx';
import Text from 'soapbox/components/ui/text.tsx'; import Text from 'soapbox/components/ui/text.tsx';
import AccountContainer from 'soapbox/containers/account-container.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useSuggestions } from 'soapbox/queries/suggestions.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'follow_recommendations.heading', defaultMessage: 'Suggested Profiles' }, heading: { id: 'follow_recommendations.heading', defaultMessage: 'Suggested Profiles' },
}); });
const FollowRecommendations: React.FC = () => { interface IFollowRecommendations {
const dispatch = useAppDispatch(); local?: boolean;
}
const FollowRecommendations: React.FC<IFollowRecommendations> = ({ local = false }) => {
const intl = useIntl(); const intl = useIntl();
const suggestions = useAppSelector((state) => state.suggestions.items); const { data: suggestions, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } = useSuggestions({ local });
const hasMore = useAppSelector((state) => !!state.suggestions.next);
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const handleLoadMore = debounce(() => { const handleLoadMore = debounce(() => {
if (isLoading) { if (hasNextPage && !isFetchingNextPage) {
return null; fetchNextPage();
} }
}, 1000);
return dispatch(fetchSuggestions({ limit: 20 })); if (suggestions.length === 0 && !isLoading) {
}, 300);
useEffect(() => {
dispatch(fetchSuggestions({ limit: 20 }));
}, []);
if (suggestions.size === 0 && !isLoading) {
return ( return (
<Column label={intl.formatMessage(messages.heading)}> <Column label={intl.formatMessage(messages.heading)}>
<Text align='center'> <Text align='center'>
@ -52,7 +44,7 @@ const FollowRecommendations: React.FC = () => {
isLoading={isLoading} isLoading={isLoading}
scrollKey='suggestions' scrollKey='suggestions'
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
hasMore={hasMore} hasMore={hasNextPage}
itemClassName='pb-4' itemClassName='pb-4'
> >
{suggestions.map((suggestion) => ( {suggestions.map((suggestion) => (

Wyświetl plik

@ -1,11 +1,13 @@
import xIcon from '@tabler/icons/outline/x.svg'; import xIcon from '@tabler/icons/outline/x.svg';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import Text from 'soapbox/components/ui/text.tsx';
import Widget from 'soapbox/components/ui/widget.tsx'; import Widget from 'soapbox/components/ui/widget.tsx';
import AccountContainer from 'soapbox/containers/account-container.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx';
import PlaceholderSidebarSuggestions from 'soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx'; import PlaceholderSidebarSuggestions from 'soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx';
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
import { useDismissSuggestion, useLocalSuggestions } from 'soapbox/queries/suggestions.ts'; import { useDismissSuggestion, useSuggestions } from 'soapbox/queries/suggestions.ts';
import type { Account as AccountEntity } from 'soapbox/types/entities.ts'; import type { Account as AccountEntity } from 'soapbox/types/entities.ts';
@ -21,7 +23,7 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit }) => {
const intl = useIntl(); const intl = useIntl();
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const { data: suggestions, isFetching } = useLocalSuggestions(); const { data: suggestions, isFetching } = useSuggestions({ local: true });
const dismissSuggestion = useDismissSuggestion(); const dismissSuggestion = useDismissSuggestion();
const suggestionsToRender = suggestions.slice(0, limit); const suggestionsToRender = suggestions.slice(0, limit);
@ -35,7 +37,16 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit }) => {
} }
return ( return (
<Widget title={<FormattedMessage id='latest_accounts.title' defaultMessage='Latest Accounts' />}> <Widget
title={<FormattedMessage id='latest_accounts.title' defaultMessage='Latest Accounts' />}
action={
<Link className='text-right' to='/suggestions/local'>
<Text tag='span' theme='primary' size='sm' className='hover:underline'>
<FormattedMessage id='feed_suggestions.view_all' defaultMessage='View all' />
</Text>
</Link>
}
>
{isFetching ? ( {isFetching ? (
<PlaceholderSidebarSuggestions limit={limit} /> <PlaceholderSidebarSuggestions limit={limit} />
) : ( ) : (

Wyświetl plik

@ -262,8 +262,9 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} /> <WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
<WrappedRoute path='/search' page={SearchPage} component={Search} content={children} publicRoute /> <WrappedRoute path='/search' page={SearchPage} component={Search} content={children} publicRoute />
{features.suggestions && <WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />} {features.suggestionsLocal && <WrappedRoute path='/suggestions/local' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} componentParams={{ local: true }} />}
{features.profileDirectory && <WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />} {features.suggestions && <WrappedRoute path='/suggestions' exact publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />}
{features.profileDirectory && <WrappedRoute path='/directory' exact publicRoute page={DefaultPage} component={Directory} content={children} />}
{features.events && <WrappedRoute path='/events' page={EventsPage} component={Events} content={children} />} {features.events && <WrappedRoute path='/events' page={EventsPage} component={Events} content={children} />}
{features.chats && <WrappedRoute path='/chats' exact page={ChatsPage} component={ChatIndex} content={children} />} {features.chats && <WrappedRoute path='/chats' exact page={ChatsPage} component={ChatIndex} content={children} />}

Wyświetl plik

@ -7,14 +7,15 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { PaginatedResult, removePageItem } from '../utils/queries.ts'; import { PaginatedResult, removePageItem } from '../utils/queries.ts';
import type { IAccount } from './accounts.ts'; import type { Account } from 'soapbox/schemas/account.ts';
type Suggestion = { type Suggestion = {
source: 'staff'; source: string;
account: IAccount; account: Account;
} }
type Result = { type Result = {
source: string;
account: string; account: string;
} }
@ -27,45 +28,48 @@ const SuggestionKeys = {
localSuggestions: ['suggestions', 'local'] as const, localSuggestions: ['suggestions', 'local'] as const,
}; };
const useSuggestions = () => { interface UseSuggestionsOpts {
local?: boolean;
}
const useSuggestions = (opts?: UseSuggestionsOpts) => {
const api = useApi(); const api = useApi();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const local = opts?.local ?? false;
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => { const getV2Suggestions = async (pageParam?: PageParam): Promise<PaginatedResult<Result>> => {
const endpoint = pageParam?.link || '/api/v2/suggestions'; const endpoint = pageParam?.link || (local ? '/api/v2/ditto/suggestions/local' : '/api/v2/suggestions');
const response = await api.get(endpoint); const response = await api.get(endpoint);
const next = response.next(); const next = response.next();
const hasMore = !!next;
const data: Suggestion[] = await response.json(); const data: Suggestion[] = await response.json();
const accounts = data.map(({ account }) => account); const accounts = data.map(({ account }) => account);
const accountIds = accounts.map((account) => account.id); const accountIds = accounts.map((account) => account.id);
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(fetchRelationships(accountIds)); dispatch(fetchRelationships(accountIds));
return { return {
result: data.map(x => ({ ...x, account: x.account.id })), result: data.map(x => ({ ...x, account: x.account.id })),
link: next ?? undefined, link: next ?? undefined,
hasMore, hasMore: !!next,
}; };
}; };
const result = useInfiniteQuery({ const result = useInfiniteQuery({
queryKey: SuggestionKeys.suggestions, queryKey: local ? SuggestionKeys.localSuggestions : SuggestionKeys.suggestions,
queryFn: ({ pageParam }: any) => getV2Suggestions(pageParam), queryFn: ({ pageParam }) => getV2Suggestions(pageParam),
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
initialPageParam: { nextLink: undefined }, initialPageParam: undefined as PageParam | undefined,
getNextPageParam: (config) => { getNextPageParam: (config): PageParam | undefined => {
if (config?.hasMore) { if (config?.hasMore) {
return { nextLink: config?.link }; return { link: config?.link };
} }
return undefined;
}, },
}); });
const data: any = result.data?.pages.reduce<Suggestion[]>( const data = result.data?.pages.reduce<Result[]>(
(prev: any, curr: any) => [...prev, ...curr.result], (prev, curr) => [...prev, ...curr.result],
[], [],
); );
@ -134,53 +138,5 @@ function useOnboardingSuggestions() {
}; };
} }
const useLocalSuggestions = () => {
const api = useApi();
const dispatch = useAppDispatch();
const getLocalSuggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => { export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion };
const endpoint = pageParam?.link || '/api/v2/ditto/suggestions/local';
const response = await api.get(endpoint);
const next = response.next();
const hasMore = !!next;
const data: Suggestion[] = await response.json();
const accounts = data.map(({ account }) => account);
const accountIds = accounts.map((account) => account.id);
dispatch(importFetchedAccounts(accounts));
dispatch(fetchRelationships(accountIds));
return {
result: data.map(x => ({ ...x, account: x.account.id })),
link: next ?? undefined,
hasMore,
};
};
const result = useInfiniteQuery({
queryKey: SuggestionKeys.localSuggestions,
queryFn: ({ pageParam }: any) => getLocalSuggestions(pageParam),
placeholderData: keepPreviousData,
initialPageParam: { nextLink: undefined },
getNextPageParam: (config) => {
if (config?.hasMore) {
return { nextLink: config?.link };
}
return undefined;
},
});
const data: any = result.data?.pages.reduce<Suggestion[]>(
(prev: any, curr: any) => [...prev, ...curr.result],
[],
);
return {
...result,
data: data || [],
};
};
export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion, useLocalSuggestions };