diff --git a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx
index a1e8581cd..4df00061a 100644
--- a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx
+++ b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx
@@ -1,49 +1,43 @@
import debounce from 'lodash/debounce';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { useDispatch } from 'react-redux';
-import { fetchSuggestions } from 'soapbox/actions/suggestions';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
-import { useAppSelector } from 'soapbox/hooks';
+import useOnboardingSuggestions from 'soapbox/queries/suggestions';
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
- const dispatch = useDispatch();
+ const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions();
- const suggestions = useAppSelector((state) => state.suggestions.items);
- const hasMore = useAppSelector((state) => !!state.suggestions.next);
- const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const handleLoadMore = debounce(() => {
- if (isLoading) {
+ if (isFetching) {
return null;
}
- return dispatch(fetchSuggestions());
+ return fetchNextPage();
}, 300);
- React.useEffect(() => {
- dispatch(fetchSuggestions({ limit: 20 }));
- }, []);
-
const renderSuggestions = () => {
+ if (!data) {
+ return null;
+ }
+
return (
- {suggestions.map((suggestion) => (
-
+ {data.map((suggestion) => (
+
, but it isn't
- id={suggestion.account}
+ id={suggestion.account.id}
showProfileHoverCard={false}
withLinkToProfile={false}
/>
@@ -65,7 +59,7 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
};
const renderBody = () => {
- if (suggestions.isEmpty()) {
+ if (!data || data.length === 0) {
return renderEmpty();
} else {
return renderSuggestions();
diff --git a/app/soapbox/queries/__tests__/suggestions.test.ts b/app/soapbox/queries/__tests__/suggestions.test.ts
new file mode 100644
index 000000000..15977dbb9
--- /dev/null
+++ b/app/soapbox/queries/__tests__/suggestions.test.ts
@@ -0,0 +1,44 @@
+import { __stub } from 'soapbox/api';
+import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
+
+import useOnboardingSuggestions from '../suggestions';
+
+describe('useCarouselAvatars', () => {
+ describe('with a successul query', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v2/suggestions')
+ .reply(200, [
+ { source: 'staff', account: { id: '1', acct: 'a', account_avatar: 'https://example.com/some.jpg' } },
+ { source: 'staff', account: { id: '2', acct: 'b', account_avatar: 'https://example.com/some.jpg' } },
+ ], {
+ link: '; rel=\'prev\'',
+ });
+ });
+ });
+
+ it('is successful', async() => {
+ const { result } = renderHook(() => useOnboardingSuggestions());
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ expect(result.current.data?.length).toBe(2);
+ });
+ });
+
+ describe('with an unsuccessul query', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v2/suggestions').networkError();
+ });
+ });
+
+ it('is successful', async() => {
+ const { result } = renderHook(() => useOnboardingSuggestions());
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ expect(result.current.error).toBeDefined();
+ });
+ });
+});
diff --git a/app/soapbox/queries/suggestions.ts b/app/soapbox/queries/suggestions.ts
new file mode 100644
index 000000000..d5ddf07bf
--- /dev/null
+++ b/app/soapbox/queries/suggestions.ts
@@ -0,0 +1,81 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+
+import { fetchRelationships } from 'soapbox/actions/accounts';
+import { importFetchedAccounts } from 'soapbox/actions/importer';
+import { getLinks } from 'soapbox/api';
+import { useApi, useAppDispatch } from 'soapbox/hooks';
+
+type Account = {
+ acct: string
+ avatar: string
+ avatar_static: string
+ bot: boolean
+ created_at: string
+ discoverable: boolean
+ display_name: string
+ followers_count: number
+ following_count: number
+ group: boolean
+ header: string
+ header_static: string
+ id: string
+ last_status_at: string
+ location: string
+ locked: boolean
+ note: string
+ statuses_count: number
+ url: string
+ username: string
+ verified: boolean
+ website: string
+}
+
+type Suggestion = {
+ source: 'staff'
+ account: Account
+}
+
+
+export default function useOnboardingSuggestions() {
+ const api = useApi();
+ const dispatch = useAppDispatch();
+
+ const getV2Suggestions = async(pageParam: any): Promise<{ data: Suggestion[], link: string | undefined, hasMore: boolean }> => {
+ const link = pageParam?.link || '/api/v2/suggestions';
+ const response = await api.get(link);
+ const hasMore = !!response.headers.link;
+ const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
+
+ const accounts = response.data.map(({ account }) => account);
+ const accountIds = accounts.map((account) => account.id);
+ dispatch(importFetchedAccounts(accounts));
+ dispatch(fetchRelationships(accountIds));
+
+ return {
+ data: response.data,
+ link: nextLink,
+ hasMore,
+ };
+ };
+
+ const result = useInfiniteQuery(['suggestions', 'v2'], ({ pageParam }) => getV2Suggestions(pageParam), {
+ keepPreviousData: true,
+ getNextPageParam: (config) => {
+ if (config.hasMore) {
+ return { link: config.link };
+ }
+
+ return undefined;
+ },
+ });
+
+ const data = result.data?.pages.reduce(
+ (prev: Suggestion[], curr) => [...prev, ...curr.data],
+ [],
+ );
+
+ return {
+ ...result,
+ data,
+ };
+}