Merge remote-tracking branch 'origin/develop' into scroll-position

This commit is contained in:
Alex Gleason 2022-05-19 20:09:56 -05:00
commit 1fb9525635
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
5 changed files with 54 additions and 20 deletions

View File

@ -1,7 +1,7 @@
import { isLoggedIn } from 'soapbox/utils/auth'; import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features'; import { getFeatures } from 'soapbox/utils/features';
import api from '../api'; import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts'; import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
@ -32,11 +32,17 @@ export function fetchSuggestionsV1(params = {}) {
export function fetchSuggestionsV2(params = {}) { export function fetchSuggestionsV2(params = {}) {
return (dispatch, getState) => { return (dispatch, getState) => {
const next = getState().getIn(['suggestions', 'next']);
dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true }); 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 accounts = suggestions.map(({ account }) => account);
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, skipLoading: true }); dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true });
return suggestions; return suggestions;
}).catch(error => { }).catch(error => {
dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true }); dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });

View File

@ -42,6 +42,8 @@ interface IScrollableList extends VirtuosoProps<any, any> {
onRefresh?: () => Promise<any>, onRefresh?: () => Promise<any>,
className?: string, className?: string,
itemClassName?: string, itemClassName?: string,
style?: React.CSSProperties,
useWindowScroll?: boolean
} }
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */ /** Legacy ScrollableList with Virtuoso for backwards-compatibility */
@ -62,6 +64,8 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
placeholderComponent: Placeholder, placeholderComponent: Placeholder,
placeholderCount = 0, placeholderCount = 0,
initialTopMostItemIndex = 0, initialTopMostItemIndex = 0,
style = {},
useWindowScroll = true,
}, ref) => { }, ref) => {
const settings = useSettings(); const settings = useSettings();
const autoloadMore = settings.get('autoloadMore'); const autoloadMore = settings.get('autoloadMore');
@ -142,7 +146,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
const renderFeed = (): JSX.Element => ( const renderFeed = (): JSX.Element => (
<Virtuoso <Virtuoso
ref={ref} ref={ref}
useWindowScroll useWindowScroll={useWindowScroll}
className={className} className={className}
data={data} data={data}
startReached={onScrollToTop} startReached={onScrollToTop}
@ -151,6 +155,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
itemContent={renderItem} itemContent={renderItem}
initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex || initialIndex.current} initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex || initialIndex.current}
rangeChanged={handleRangeChanged} rangeChanged={handleRangeChanged}
style={style}
context={{ context={{
listClassName: className, listClassName: className,
itemClassName, itemClassName,

View File

@ -1,8 +1,10 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import debounce from 'lodash/debounce';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container'; import AccountContainer from 'soapbox/containers/account_container';
import { useAppSelector } from 'soapbox/hooks'; import { useAppSelector } from 'soapbox/hooks';
@ -13,24 +15,42 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const suggestions = useAppSelector((state) => state.suggestions.get('items')); 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(() => { React.useEffect(() => {
dispatch(fetchSuggestions()); dispatch(fetchSuggestions({ limit: 20 }));
}, []); }, []);
const renderSuggestions = () => { const renderSuggestions = () => {
return ( return (
<div className='sm:pt-4 sm:pb-10 flex flex-col divide-y divide-solid divide-gray-200 dark:divide-slate-700'> <div className='sm:pt-4 sm:pb-10 flex flex-col'>
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => ( <ScrollableList
<div key={suggestion.get('account')} className='py-2'> isLoading={isLoading}
<AccountContainer scrollKey='suggestions'
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't onLoadMore={handleLoadMore}
id={suggestion.get('account')} hasMore={hasMore}
showProfileHoverCard={false} useWindowScroll={false}
/> style={{ height: 320 }}
</div> >
))} {suggestions.map((suggestion: ImmutableMap<string, any>) => (
<div key={suggestion.get('account')} className='py-2'>
<AccountContainer
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
id={suggestion.get('account')}
showProfileHoverCard={false}
/>
</div>
))}
</ScrollableList>
</div> </div>
); );
}; };
@ -46,7 +66,7 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
}; };
const renderBody = () => { const renderBody = () => {
if (suggestionsToRender.isEmpty()) { if (suggestions.isEmpty()) {
return renderEmpty(); return renderEmpty();
} else { } else {
return renderSuggestions(); return renderSuggestions();

View File

@ -8,6 +8,7 @@ describe('suggestions reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({ expect(reducer(undefined, {})).toEqual(ImmutableMap({
items: ImmutableList(), items: ImmutableList(),
next: null,
isLoading: false, isLoading: false,
})); }));
}); });

View File

@ -15,6 +15,7 @@ import {
const initialState = ImmutableMap({ const initialState = ImmutableMap({
items: ImmutableList(), items: ImmutableList(),
next: null,
isLoading: false, isLoading: false,
}); });
@ -33,10 +34,11 @@ const importAccounts = (state, accounts) => {
}); });
}; };
const importSuggestions = (state, suggestions) => { const importSuggestions = (state, suggestions, next) => {
return state.withMutations(state => { 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('isLoading', false);
state.set('next', next);
}); });
}; };
@ -56,7 +58,7 @@ export default function suggestionsReducer(state = initialState, action) {
case SUGGESTIONS_FETCH_SUCCESS: case SUGGESTIONS_FETCH_SUCCESS:
return importAccounts(state, action.accounts); return importAccounts(state, action.accounts);
case SUGGESTIONS_V2_FETCH_SUCCESS: case SUGGESTIONS_V2_FETCH_SUCCESS:
return importSuggestions(state, action.suggestions); return importSuggestions(state, action.suggestions, action.next);
case SUGGESTIONS_FETCH_FAIL: case SUGGESTIONS_FETCH_FAIL:
case SUGGESTIONS_V2_FETCH_FAIL: case SUGGESTIONS_V2_FETCH_FAIL:
return state.set('isLoading', false); return state.set('isLoading', false);