From 1d69b66e4bdd12f6a00c411f2a41b200c931925e Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 26 Sep 2022 15:22:00 -0400 Subject: [PATCH 1/6] Use React Query for suggestions --- app/soapbox/components/ui/widget/widget.tsx | 2 +- .../placeholder-sidebar-suggestions.tsx | 29 +++++ .../ui/components/who-to-follow-panel.tsx | 58 ++++----- app/soapbox/pages/default_page.tsx | 2 +- app/soapbox/pages/home_page.tsx | 2 +- app/soapbox/pages/profile_page.tsx | 2 +- app/soapbox/pages/status_page.tsx | 2 +- app/soapbox/queries/suggestions.ts | 122 +++++++++++++++++- app/soapbox/utils/queries.ts | 61 +++++++++ 9 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx create mode 100644 app/soapbox/utils/queries.ts diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx index 7b966a6ef..c4be654b3 100644 --- a/app/soapbox/components/ui/widget/widget.tsx +++ b/app/soapbox/components/ui/widget/widget.tsx @@ -42,7 +42,7 @@ const Widget: React.FC = ({ }): JSX.Element => { return ( - + {action || (onActionClick && ( { + const length = randomIntFromInterval(15, 3); + const acctLength = randomIntFromInterval(15, 3); + + return ( + <> + {new Array(limit).fill(undefined).map((_, idx) => ( + + +
+ + + +

{generateText(length)}

+

{generateText(acctLength)}

+
+ + ))} + + ); +}; \ No newline at end of file diff --git a/app/soapbox/features/ui/components/who-to-follow-panel.tsx b/app/soapbox/features/ui/components/who-to-follow-panel.tsx index 2458997f4..418d2143c 100644 --- a/app/soapbox/features/ui/components/who-to-follow-panel.tsx +++ b/app/soapbox/features/ui/components/who-to-follow-panel.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; +import { Link } from 'react-router-dom'; -import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions'; -import { Widget } from 'soapbox/components/ui'; +import { Text, Widget } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; -import { useAppSelector } from 'soapbox/hooks'; +import PlaceholderSidebarSuggestions from 'soapbox/features/placeholder/components/placeholder-sidebar-suggestions'; +import { useDismissSuggestion, useSuggestions } from 'soapbox/queries/suggestions'; import type { Account as AccountEntity } from 'soapbox/types/entities'; @@ -18,44 +18,40 @@ interface IWhoToFollowPanel { } const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => { - const dispatch = useDispatch(); const intl = useIntl(); - const suggestions = useAppSelector((state) => state.suggestions.items); + const { data: suggestions, isFetching } = useSuggestions(); + const dismissSuggestion = useDismissSuggestion(); + const suggestionsToRender = suggestions.slice(0, limit); const handleDismiss = (account: AccountEntity) => { - dispatch(dismissSuggestion(account.id)); + dismissSuggestion.mutate(account.id); }; - React.useEffect(() => { - dispatch(fetchSuggestions()); - }, []); - - if (suggestionsToRender.isEmpty()) { - return null; - } - - // FIXME: This page actually doesn't look good right now - // const handleAction = () => { - // history.push('/suggestions'); - // }; - return ( } - // onAction={handleAction} + action={ + + View all + + } > - {suggestionsToRender.map((suggestion) => ( - , but it isn't - id={suggestion.account} - actionIcon={require('@tabler/icons/x.svg')} - actionTitle={intl.formatMessage(messages.dismissSuggestion)} - onActionClick={handleDismiss} - /> - ))} + {isFetching ? ( + + ) : ( + suggestionsToRender.map((suggestion: any) => ( + , but it isn't + id={suggestion.account} + actionIcon={require('@tabler/icons/x.svg')} + actionTitle={intl.formatMessage(messages.dismissSuggestion)} + onActionClick={handleDismiss} + /> + )) + )} ); }; diff --git a/app/soapbox/pages/default_page.tsx b/app/soapbox/pages/default_page.tsx index b64cf452d..c013eb63d 100644 --- a/app/soapbox/pages/default_page.tsx +++ b/app/soapbox/pages/default_page.tsx @@ -41,7 +41,7 @@ const DefaultPage: React.FC = ({ children }) => { )} {features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/pages/home_page.tsx b/app/soapbox/pages/home_page.tsx index 23bd3e3c7..dc8c2c9f4 100644 --- a/app/soapbox/pages/home_page.tsx +++ b/app/soapbox/pages/home_page.tsx @@ -105,7 +105,7 @@ const HomePage: React.FC = ({ children }) => { )} {features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/pages/profile_page.tsx b/app/soapbox/pages/profile_page.tsx index f4ff974e0..9f57e23ab 100644 --- a/app/soapbox/pages/profile_page.tsx +++ b/app/soapbox/pages/profile_page.tsx @@ -139,7 +139,7 @@ const ProfilePage: React.FC = ({ params, children }) => { ) : features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/pages/status_page.tsx b/app/soapbox/pages/status_page.tsx index 2c35947ad..414df6783 100644 --- a/app/soapbox/pages/status_page.tsx +++ b/app/soapbox/pages/status_page.tsx @@ -45,7 +45,7 @@ const StatusPage: React.FC = ({ children }) => { )} {features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/queries/suggestions.ts b/app/soapbox/queries/suggestions.ts index 433a0c940..8c9e2ec20 100644 --- a/app/soapbox/queries/suggestions.ts +++ b/app/soapbox/queries/suggestions.ts @@ -1,9 +1,12 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { fetchRelationships } from 'soapbox/actions/accounts'; import { importFetchedAccounts } from 'soapbox/actions/importer'; +import { SuggestedProfile } from 'soapbox/actions/suggestions'; import { getLinks } from 'soapbox/api'; -import { useApi, useAppDispatch } from 'soapbox/hooks'; +import { useApi, useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks'; + +import { PaginatedResult, removePageItem } from '../utils/queries'; type Account = { acct: string @@ -35,7 +38,118 @@ type Suggestion = { account: Account } -export default function useOnboardingSuggestions() { +type TruthSuggestion = { + account_avatar: string + account_id: string + acct: string + display_name: string + note: string + verified: boolean +} + +type PageParam = { + link?: string +} + +const suggestionKeys = { + suggestions: ['suggestions'] as const, +}; + +const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({ + id: suggestedProfile.account_id, + avatar: suggestedProfile.account_avatar, + avatar_static: suggestedProfile.account_avatar, + acct: suggestedProfile.acct, + display_name: suggestedProfile.display_name, + note: suggestedProfile.note, + verified: suggestedProfile.verified, +}); + +const useSuggestions = () => { + const account = useOwnAccount(); + const api = useApi(); + const dispatch = useAppDispatch(); + const features = useFeatures(); + + const getV2Suggestions = async(pageParam: PageParam): Promise> => { + const endpoint = pageParam?.link || '/api/v2/suggestions'; + const response = await api.get(endpoint); + 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 { + result: response.data, + link: nextLink, + hasMore, + }; + }; + + const getTruthSuggestions = async(pageParam: PageParam): Promise> => { + const endpoint = pageParam?.link || '/api/v1/truth/carousels/suggestions'; + const response = await api.get(endpoint); + const hasMore = !!response.headers.link; + const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri; + + const accounts = response.data.map(mapSuggestedProfileToAccount); + dispatch(importFetchedAccounts(accounts, { should_refetch: true })); + + return { + result: response.data.map((x) => ({ ...x, account: x.account_id })), + link: nextLink, + hasMore, + }; + }; + + const getSuggestions = (pageParam: PageParam) => { + if (features.truthSuggestions) { + return getTruthSuggestions(pageParam); + } else { + return getV2Suggestions(pageParam); + } + }; + + const result = useInfiniteQuery( + suggestionKeys.suggestions, + ({ pageParam }: any) => getSuggestions(pageParam), + { + keepPreviousData: true, + enabled: !!account, + getNextPageParam: (config) => { + if (config?.hasMore) { + return { nextLink: config?.link }; + } + + return undefined; + }, + }); + + const data: any = result.data?.pages.reduce( + (prev: any, curr: any) => [...prev, ...curr.result], + [], + ); + + return { + ...result, + data: data || [], + }; +}; + +const useDismissSuggestion = () => { + const api = useApi(); + + return useMutation((accountId: string) => api.delete(`/api/v1/suggestions/${accountId}`), { + onMutate(accountId: string) { + removePageItem(suggestionKeys.suggestions, accountId, (o: any, n: any) => o.account_id === n); + }, + }); +}; + +function useOnboardingSuggestions() { const api = useApi(); const dispatch = useAppDispatch(); @@ -78,3 +192,5 @@ export default function useOnboardingSuggestions() { data, }; } + +export { useOnboardingSuggestions as default, useSuggestions, useDismissSuggestion }; \ No newline at end of file diff --git a/app/soapbox/utils/queries.ts b/app/soapbox/utils/queries.ts new file mode 100644 index 000000000..d066f0855 --- /dev/null +++ b/app/soapbox/utils/queries.ts @@ -0,0 +1,61 @@ +import { queryClient } from 'soapbox/queries/client'; + +import type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query'; + +export interface PaginatedResult { + result: T[], + hasMore: boolean, + link?: string, +} + +/** Flatten paginated results into a single array. */ +const flattenPages = (queryInfo: UseInfiniteQueryResult>) => { + return queryInfo.data?.pages.reduce( + (prev: T[], curr) => [...prev, ...curr.result], + [], + ); +}; + +/** Traverse pages and update the item inside if found. */ +const updatePageItem = (queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => { + queryClient.setQueriesData>>(queryKey, (data) => { + if (data) { + const pages = data.pages.map(page => { + const result = page.result.map(item => isItem(item, newItem) ? newItem : item); + return { ...page, result }; + }); + return { ...data, pages }; + } + }); +}; + +/** Insert the new item at the beginning of the first page. */ +const appendPageItem = (queryKey: QueryKey, newItem: T) => { + queryClient.setQueryData>>(queryKey, (data) => { + if (data) { + const pages = [...data.pages]; + pages[0] = { ...pages[0], result: [...pages[0].result, newItem] }; + return { ...data, pages }; + } + }); +}; + +/** Remove an item inside if found. */ +const removePageItem = (queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => { + queryClient.setQueriesData>>(queryKey, (data) => { + if (data) { + const pages = data.pages.map(page => { + const result = page.result.filter(item => !isItem(item, itemToRemove)); + return { ...page, result }; + }); + return { ...data, pages }; + } + }); +}; + +export { + flattenPages, + updatePageItem, + appendPageItem, + removePageItem, +}; From 63bd9a21fc93db585c19619b4f5edc0363fc6c23 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 26 Sep 2022 15:23:51 -0400 Subject: [PATCH 2/6] Update spacing on Widgets --- app/soapbox/components/ui/widget/widget.tsx | 2 +- app/soapbox/features/feed-suggestions/feed-suggestions.tsx | 4 ++-- app/soapbox/features/follow-recommendations/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx index c4be654b3..3bced193f 100644 --- a/app/soapbox/components/ui/widget/widget.tsx +++ b/app/soapbox/components/ui/widget/widget.tsx @@ -41,7 +41,7 @@ const Widget: React.FC = ({ action, }): JSX.Element => { return ( - + {action || (onActionClick && ( diff --git a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx index d946f5e8c..2545c72e5 100644 --- a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx +++ b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx @@ -11,7 +11,7 @@ import ActionButton from '../ui/components/action-button'; import type { Account } from 'soapbox/types/entities'; const messages = defineMessages({ - heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested profiles' }, + heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested Profiles' }, viewAll: { id: 'feed_suggestions.view_all', defaultMessage: 'View all' }, }); @@ -65,7 +65,7 @@ const FeedSuggestions = () => { if (!isLoading && suggestedProfiles.size === 0) return null; return ( - + diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 7fda03c7a..221ca1491 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -10,7 +10,7 @@ import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; const messages = defineMessages({ - heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested profiles' }, + heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested Profiles' }, }); const FollowRecommendations: React.FC = () => { From a0d595c7df8f056921cd1ec017f62dcff2d2cba0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 26 Sep 2022 15:00:01 -0500 Subject: [PATCH 3/6] CountryCodeDropdown: support +55, +351 --- app/soapbox/utils/phone.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/utils/phone.ts b/app/soapbox/utils/phone.ts index 9cc175f5d..8fde39f12 100644 --- a/app/soapbox/utils/phone.ts +++ b/app/soapbox/utils/phone.ts @@ -1,7 +1,9 @@ /** List of supported E164 country codes. */ const COUNTRY_CODES = [ '1', + '351', '44', + '55', ] as const; /** Supported E164 country code. */ From facd4e95f53b83d0d3f6feef3e456e2d75d73a07 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 27 Sep 2022 09:58:49 -0400 Subject: [PATCH 4/6] Add tests for new Who To Follow panel --- .../steps/suggested-accounts-step.tsx | 2 +- .../placeholder-sidebar-suggestions.tsx | 2 +- .../__tests__/who-to-follow-panel.test.tsx | 294 +++++++++++------- .../queries/__tests__/suggestions.test.ts | 6 +- app/soapbox/queries/suggestions.ts | 18 +- 5 files changed, 201 insertions(+), 121 deletions(-) diff --git a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx index 8db08d1a4..a05202ff9 100644 --- a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx +++ b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; 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 useOnboardingSuggestions from 'soapbox/queries/suggestions'; +import { useOnboardingSuggestions } from 'soapbox/queries/suggestions'; const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions(); diff --git a/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx b/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx index 757c47cc7..e96268cb2 100644 --- a/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx +++ b/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx @@ -11,7 +11,7 @@ export default ({ limit }: { limit: number }) => { return ( <> {new Array(limit).fill(undefined).map((_, idx) => ( - +
({ + account_avatar: 'avatar', + account_id: id, + acct: 'acct', + display_name: 'my name', + note: 'hello', + verified: true, +}); + +const buildSuggestion = (id: string) => ({ + source: 'staff', + account: { + username: 'username', + verified: true, + id, + acct: 'acct', + avatar: 'avatar', + avatar_static: 'avatar', + display_name: 'my name', + }, +}); + describe('', () => { - it('renders suggested accounts', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([{ - source: 'staff', - account: '1', - }]), - }, - }; + let store: any; - render(, undefined, store); - expect(screen.getByTestId('account')).toHaveTextContent(/my name/i); + describe('using Truth Social software', () => { + beforeEach(() => { + store = rootState + .set('me', '1234') + .set('instance', normalizeInstance({ + version: '3.4.1 (compatible; TruthSocial 1.0.0)', + })); + }); + + describe('with a single suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [buildTruthSuggestion('1')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.getByTestId('account')).toHaveTextContent(/my name/i); + }); + }); + }); + + describe('with a multiple suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [buildTruthSuggestion('1'), buildTruthSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(2); + }); + }); + }); + + describe('with a set limit', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [buildTruthSuggestion('1'), buildTruthSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('respects the limit prop', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(1); + }); + }); + }); + + describe('when the API returns an empty list', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [], { + link: '', + }); + }); + }); + + it('renders empty', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(0); + }); + }); + }); }); - it('renders multiple accounts', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - '2': normalizeAccount({ - id: '1', - acct: 'username2', - display_name: 'My other name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([ - { - source: 'staff', - account: '1', - }, - { - source: 'staff', - account: '2', - }, - ]), - }, - }; + describe('using Pleroma software', () => { + beforeEach(() => { + store = rootState.set('me', '1234'); + }); - render(, undefined, store); - expect(screen.queryAllByTestId('account')).toHaveLength(2); - }); + describe('with a single suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [buildSuggestion('1')], { + link: '; rel=\'prev\'', + }); + }); + }); - it('respects the limit prop', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - '2': normalizeAccount({ - id: '1', - acct: 'username2', - display_name: 'My other name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([ - { - source: 'staff', - account: '1', - }, - { - source: 'staff', - account: '2', - }, - ]), - }, - }; + it('renders suggested accounts', async () => { + render(, undefined, store); - render(, undefined, store); - expect(screen.queryAllByTestId('account')).toHaveLength(1); - }); + await waitFor(() => { + expect(screen.getByTestId('account')).toHaveTextContent(/my name/i); + }); + }); + }); - it('renders empty', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - '2': normalizeAccount({ - id: '1', - acct: 'username2', - display_name: 'My other name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([]), - }, - }; + describe('with a multiple suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [buildSuggestion('1'), buildSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); - render(, undefined, store); - expect(screen.queryAllByTestId('account')).toHaveLength(0); + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(2); + }); + }); + }); + + describe('with a set limit', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [buildSuggestion('1'), buildSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('respects the limit prop', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(1); + }); + }); + }); + + describe('when the API returns an empty list', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [], { + link: '', + }); + }); + }); + + it('renders empty', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(0); + }); + }); + }); }); }); diff --git a/app/soapbox/queries/__tests__/suggestions.test.ts b/app/soapbox/queries/__tests__/suggestions.test.ts index f38bf0dbc..aa352abe9 100644 --- a/app/soapbox/queries/__tests__/suggestions.test.ts +++ b/app/soapbox/queries/__tests__/suggestions.test.ts @@ -1,7 +1,7 @@ import { __stub } from 'soapbox/api'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; -import useOnboardingSuggestions from '../suggestions'; +import { useOnboardingSuggestions } from '../suggestions'; describe('useCarouselAvatars', () => { describe('with a successful query', () => { @@ -17,7 +17,7 @@ describe('useCarouselAvatars', () => { }); }); - it('is successful', async() => { + it('is successful', async () => { const { result } = renderHook(() => useOnboardingSuggestions()); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -33,7 +33,7 @@ describe('useCarouselAvatars', () => { }); }); - it('is successful', async() => { + it('is successful', async () => { const { result } = renderHook(() => useOnboardingSuggestions()); await waitFor(() => expect(result.current.isFetching).toBe(false)); diff --git a/app/soapbox/queries/suggestions.ts b/app/soapbox/queries/suggestions.ts index 8c9e2ec20..50b624831 100644 --- a/app/soapbox/queries/suggestions.ts +++ b/app/soapbox/queries/suggestions.ts @@ -4,7 +4,7 @@ import { fetchRelationships } from 'soapbox/actions/accounts'; import { importFetchedAccounts } from 'soapbox/actions/importer'; import { SuggestedProfile } from 'soapbox/actions/suggestions'; import { getLinks } from 'soapbox/api'; -import { useApi, useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks'; +import { useApi, useAppDispatch, useFeatures } from 'soapbox/hooks'; import { PaginatedResult, removePageItem } from '../utils/queries'; @@ -47,6 +47,10 @@ type TruthSuggestion = { verified: boolean } +type Result = TruthSuggestion | { + account: string +} + type PageParam = { link?: string } @@ -66,12 +70,11 @@ const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({ }); const useSuggestions = () => { - const account = useOwnAccount(); const api = useApi(); const dispatch = useAppDispatch(); const features = useFeatures(); - const getV2Suggestions = async(pageParam: PageParam): Promise> => { + const getV2Suggestions = async (pageParam: PageParam): Promise> => { const endpoint = pageParam?.link || '/api/v2/suggestions'; const response = await api.get(endpoint); const hasMore = !!response.headers.link; @@ -83,13 +86,13 @@ const useSuggestions = () => { dispatch(fetchRelationships(accountIds)); return { - result: response.data, + result: response.data.map(x => ({ ...x, account: x.account.id })), link: nextLink, hasMore, }; }; - const getTruthSuggestions = async(pageParam: PageParam): Promise> => { + const getTruthSuggestions = async (pageParam: PageParam): Promise> => { const endpoint = pageParam?.link || '/api/v1/truth/carousels/suggestions'; const response = await api.get(endpoint); const hasMore = !!response.headers.link; @@ -118,7 +121,6 @@ const useSuggestions = () => { ({ pageParam }: any) => getSuggestions(pageParam), { keepPreviousData: true, - enabled: !!account, getNextPageParam: (config) => { if (config?.hasMore) { return { nextLink: config?.link }; @@ -153,7 +155,7 @@ function useOnboardingSuggestions() { const api = useApi(); const dispatch = useAppDispatch(); - const getV2Suggestions = async(pageParam: any): Promise<{ data: Suggestion[], link: string | undefined, hasMore: boolean }> => { + 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; @@ -193,4 +195,4 @@ function useOnboardingSuggestions() { }; } -export { useOnboardingSuggestions as default, useSuggestions, useDismissSuggestion }; \ No newline at end of file +export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion }; \ No newline at end of file From b34b65cb90cf2267ef0a167150bcd3cb7aeb85dd Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 27 Sep 2022 09:59:01 -0400 Subject: [PATCH 5/6] Fix conflict between eslint / typescript --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 164949e65..0ecb15a5b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -277,6 +277,7 @@ module.exports = { files: ['**/*.ts', '**/*.tsx'], rules: { 'no-undef': 'off', // https://stackoverflow.com/a/69155899 + 'space-before-function-paren': 'off', }, parser: '@typescript-eslint/parser', }, From d4cf5dc2b9d90618cb2dab77fffa689e5bb42cb7 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 27 Sep 2022 10:35:35 -0400 Subject: [PATCH 6/6] Remove unneeded disabled button on Bio step We don't require users to have bios, so we shouldn't force them to add one during the onboarding steps in order to proceed to the next step without "skipping" it. --- app/soapbox/features/onboarding/steps/bio-step.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/soapbox/features/onboarding/steps/bio-step.tsx b/app/soapbox/features/onboarding/steps/bio-step.tsx index 895fd08e8..31c7bc6c9 100644 --- a/app/soapbox/features/onboarding/steps/bio-step.tsx +++ b/app/soapbox/features/onboarding/steps/bio-step.tsx @@ -17,10 +17,6 @@ const BioStep = ({ onNext }: { onNext: () => void }) => { const [isSubmitting, setSubmitting] = React.useState(false); const [errors, setErrors] = React.useState([]); - const trimmedValue = value.trim(); - const isValid = trimmedValue.length > 0; - const isDisabled = !isValid; - const handleSubmit = () => { setSubmitting(true); @@ -79,7 +75,7 @@ const BioStep = ({ onNext }: { onNext: () => void }) => { block theme='primary' type='submit' - disabled={isDisabled || isSubmitting} + disabled={isSubmitting} onClick={handleSubmit} > {isSubmitting ? (