From ae0fd07580c3f8032ddf5d5f53a6ebad41974128 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 9 Aug 2022 11:00:22 -0400 Subject: [PATCH] Use v2 suggestions endpoint for Onboarding --- .../steps/suggested-accounts-step.tsx | 34 ++++---- .../queries/__tests__/suggestions.test.ts | 45 ++++++++++ app/soapbox/queries/suggestions.ts | 82 +++++++++++++++++++ 3 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 app/soapbox/queries/__tests__/suggestions.test.ts create mode 100644 app/soapbox/queries/suggestions.ts 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..3a440d984 --- /dev/null +++ b/app/soapbox/queries/__tests__/suggestions.test.ts @@ -0,0 +1,45 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { mock, queryWrapper, waitFor } from 'soapbox/jest/test-helpers'; + +import useOnboardingSuggestions from '../suggestions'; + +describe('useCarouselAvatars', () => { + describe('with a successul query', () => { + beforeEach(() => { + 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(), { + wrapper: queryWrapper, + }); + + await waitFor(() => expect(result.current.isFetching).toBe(false)); + + expect(result.current.data?.length).toBe(2); + }); + }); + + describe('with an unsuccessul query', () => { + beforeEach(() => { + mock.onGet('/api/v2/suggestions').networkError(); + }); + + it('is successful', async() => { + const { result } = renderHook(() => useOnboardingSuggestions(), { + wrapper: queryWrapper, + }); + + 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..12acfb895 --- /dev/null +++ b/app/soapbox/queries/suggestions.ts @@ -0,0 +1,82 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { fetchRelationships } from 'soapbox/actions/accounts'; +import { importFetchedAccounts } from 'soapbox/actions/importer'; +import { getLinks } from 'soapbox/api'; +import { useAppDispatch } from 'soapbox/hooks'; +import API from 'soapbox/queries/client'; + +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 +} + +const getV2Suggestions = async(dispatch: any, pageParam: any): Promise<{ data: Suggestion[], link: string | null, hasMore: boolean }> => { + return dispatch(async() => { + 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, + }; + }); +}; + +export default function useOnboardingSuggestions() { + const dispatch = useAppDispatch(); + + const result = useInfiniteQuery(['suggestions', 'v2'], ({ pageParam }) => getV2Suggestions(dispatch, 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, + }; +}