From c6c65a0d48ca14d1c89eac8e172985728e91b4d4 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 19 May 2022 12:29:28 -0400 Subject: [PATCH 1/3] Add infinite scroll to suggestions onboarding --- app/soapbox/actions/suggestions.js | 12 +++-- app/soapbox/components/scrollable_list.tsx | 7 ++- app/soapbox/containers/soapbox.tsx | 2 +- .../steps/suggested-accounts-step.tsx | 46 +++++++++++++------ app/soapbox/reducers/suggestions.js | 8 ++-- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/soapbox/actions/suggestions.js b/app/soapbox/actions/suggestions.js index ede37ff30..c804c4855 100644 --- a/app/soapbox/actions/suggestions.js +++ b/app/soapbox/actions/suggestions.js @@ -1,7 +1,7 @@ import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures } from 'soapbox/utils/features'; -import api from '../api'; +import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; @@ -32,11 +32,17 @@ export function fetchSuggestionsV1(params = {}) { export function fetchSuggestionsV2(params = {}) { return (dispatch, getState) => { + const next = getState().getIn(['suggestions', 'next']); + dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true }); - return api(getState).get('/api/v2/suggestions', { params }).then(({ data: suggestions }) => { + + return api(getState).get(next ? next.uri : '/api/v2/suggestions', next ? {} : { params }).then((response) => { + const suggestions = response.data; const accounts = suggestions.map(({ account }) => account); + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(accounts)); - dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, skipLoading: true }); + dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true }); return suggestions; }).catch(error => { dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true }); diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index cc4b55867..c2497547b 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -42,6 +42,8 @@ interface IScrollableList extends VirtuosoProps { onRefresh?: () => Promise, className?: string, itemClassName?: string, + style?: React.CSSProperties, + useWindowScroll?: boolean } /** Legacy ScrollableList with Virtuoso for backwards-compatibility */ @@ -63,6 +65,8 @@ const ScrollableList = React.forwardRef(({ placeholderCount = 0, initialTopMostItemIndex = 0, scrollerRef, + style = {}, + useWindowScroll = true, }, ref) => { const settings = useSettings(); const autoloadMore = settings.get('autoloadMore'); @@ -129,7 +133,7 @@ const ScrollableList = React.forwardRef(({ const renderFeed = (): JSX.Element => ( (({ isScrolling={isScrolling => isScrolling && onScroll && onScroll()} itemContent={renderItem} initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex} + style={style} context={{ listClassName: className, itemClassName, diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index cde450e65..79329f300 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -48,7 +48,7 @@ store.dispatch(checkOnboardingStatus() as any); /** Load initial data from the backend */ const loadInitial = () => { // @ts-ignore - return async(dispatch, getState) => { + return async (dispatch, getState) => { // Await for authenticated fetch await dispatch(fetchMe()); // Await for feature detection diff --git a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx index d3972e1ab..4421979cb 100644 --- a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx +++ b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx @@ -1,8 +1,10 @@ import { Map as ImmutableMap } from 'immutable'; +import debounce from 'lodash/debounce'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; +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'; @@ -13,24 +15,42 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { const dispatch = useDispatch(); const suggestions = useAppSelector((state) => state.suggestions.get('items')); - const suggestionsToRender = suggestions.slice(0, 5); + const hasMore = useAppSelector((state) => !!state.suggestions.get('next')); + const isLoading = useAppSelector((state) => state.suggestions.get('isLoading')); + + const handleLoadMore = debounce(() => { + if (isLoading) { + return null; + } + + return dispatch(fetchSuggestions()); + }, 300); React.useEffect(() => { - dispatch(fetchSuggestions()); + dispatch(fetchSuggestions({ limit: 20 })); }, []); const renderSuggestions = () => { return ( -
- {suggestionsToRender.map((suggestion: ImmutableMap) => ( -
- , but it isn't - id={suggestion.get('account')} - showProfileHoverCard={false} - /> -
- ))} +
+ + {suggestions.map((suggestion: ImmutableMap) => ( +
+ , but it isn't + id={suggestion.get('account')} + showProfileHoverCard={false} + /> +
+ ))} +
); }; @@ -46,7 +66,7 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { }; const renderBody = () => { - if (suggestionsToRender.isEmpty()) { + if (suggestions.isEmpty()) { return renderEmpty(); } else { return renderSuggestions(); diff --git a/app/soapbox/reducers/suggestions.js b/app/soapbox/reducers/suggestions.js index ac00eecf1..03f782d3e 100644 --- a/app/soapbox/reducers/suggestions.js +++ b/app/soapbox/reducers/suggestions.js @@ -15,6 +15,7 @@ import { const initialState = ImmutableMap({ items: ImmutableList(), + next: null, isLoading: false, }); @@ -33,10 +34,11 @@ const importAccounts = (state, accounts) => { }); }; -const importSuggestions = (state, suggestions) => { +const importSuggestions = (state, suggestions, next) => { return state.withMutations(state => { - state.set('items', fromJS(suggestions.map(x => ({ ...x, account: x.account.id })))); + state.update('items', items => items.concat(fromJS(suggestions.map(x => ({ ...x, account: x.account.id }))))); state.set('isLoading', false); + state.set('next', next); }); }; @@ -56,7 +58,7 @@ export default function suggestionsReducer(state = initialState, action) { case SUGGESTIONS_FETCH_SUCCESS: return importAccounts(state, action.accounts); case SUGGESTIONS_V2_FETCH_SUCCESS: - return importSuggestions(state, action.suggestions); + return importSuggestions(state, action.suggestions, action.next); case SUGGESTIONS_FETCH_FAIL: case SUGGESTIONS_V2_FETCH_FAIL: return state.set('isLoading', false); From 3fbe81040636c3b74426b8bb5eeffb0d574035f5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 19 May 2022 11:52:36 -0500 Subject: [PATCH 2/3] Crom, I have never prayed to you before. I have no tongue for it. --- app/soapbox/containers/soapbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 79329f300..cde450e65 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -48,7 +48,7 @@ store.dispatch(checkOnboardingStatus() as any); /** Load initial data from the backend */ const loadInitial = () => { // @ts-ignore - return async (dispatch, getState) => { + return async(dispatch, getState) => { // Await for authenticated fetch await dispatch(fetchMe()); // Await for feature detection From cee7e3761fa3b2191338033135ec1af466f01056 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 19 May 2022 12:31:16 -0500 Subject: [PATCH 3/3] Fix suggestions test --- app/soapbox/reducers/__tests__/suggestions-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/reducers/__tests__/suggestions-test.js b/app/soapbox/reducers/__tests__/suggestions-test.js index 7da0b7f75..01c1f1aff 100644 --- a/app/soapbox/reducers/__tests__/suggestions-test.js +++ b/app/soapbox/reducers/__tests__/suggestions-test.js @@ -8,6 +8,7 @@ describe('suggestions reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap({ items: ImmutableList(), + next: null, isLoading: false, })); });