From a985348bf1f38cda18ad9f2a48418632444bf471 Mon Sep 17 00:00:00 2001 From: oakes Date: Thu, 15 Jun 2023 09:05:55 -0400 Subject: [PATCH] Optionally use Link header for search pagination --- app/soapbox/actions/search.ts | 41 +++++++++++++++++++++++----------- app/soapbox/reducers/search.ts | 11 +++++---- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/soapbox/actions/search.ts b/app/soapbox/actions/search.ts index 3f8d2011e..2b4c8f4e9 100644 --- a/app/soapbox/actions/search.ts +++ b/app/soapbox/actions/search.ts @@ -1,4 +1,4 @@ -import api from '../api'; +import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatuses } from './importer'; @@ -83,7 +83,9 @@ const submitSearch = (filter?: SearchFilter) => dispatch(importFetchedStatuses(response.data.statuses)); } - dispatch(fetchSearchSuccess(response.data, value, type)); + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(fetchSearchSuccess(response.data, value, type, next ? next.uri : null)); dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); @@ -95,11 +97,12 @@ const fetchSearchRequest = (value: string) => ({ value, }); -const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({ +const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({ type: SEARCH_FETCH_SUCCESS, results, searchTerm, searchType, + next, }); const fetchSearchFail = (error: AxiosError) => ({ @@ -125,17 +128,26 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( dispatch(expandSearchRequest(type)); - const params: Record = { - q: value, - type, - offset, - }; + let url = getState().search.next as string; + let params: Record = {}; - if (accountId) params.account_id = accountId; + // if no URL was extracted from the Link header, + // fall back on querying with the offset + if (!url) { + url = '/api/v2/search'; + params = { + q: value, + type, + offset, + }; + if (accountId) params.account_id = accountId; + } - api(getState).get('/api/v2/search', { + api(getState).get(url, { params, - }).then(({ data }) => { + }).then(response => { + const data = response.data; + if (data.accounts) { dispatch(importFetchedAccounts(data.accounts)); } @@ -144,7 +156,9 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( dispatch(importFetchedStatuses(data.statuses)); } - dispatch(expandSearchSuccess(data, value, type)); + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(expandSearchSuccess(data, value, type, next ? next.uri : null)); dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(expandSearchFail(error)); @@ -156,11 +170,12 @@ const expandSearchRequest = (searchType: SearchFilter) => ({ searchType, }); -const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({ +const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({ type: SEARCH_EXPAND_SUCCESS, results, searchTerm, searchType, + next, }); const expandSearchFail = (error: AxiosError) => ({ diff --git a/app/soapbox/reducers/search.ts b/app/soapbox/reducers/search.ts index abc633533..f4e299290 100644 --- a/app/soapbox/reducers/search.ts +++ b/app/soapbox/reducers/search.ts @@ -47,6 +47,7 @@ const ReducerRecord = ImmutableRecord({ results: ResultsRecord(), filter: 'accounts' as SearchFilter, accountId: null as string | null, + next: null as string | null, }); type State = ReturnType; @@ -57,7 +58,7 @@ const toIds = (items: APIEntities = []) => { return ImmutableOrderedSet(items.map(item => item.id)); }; -const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter) => { +const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter, next: string | null) => { return state.withMutations(state => { if (state.value === searchTerm && state.filter === searchType) { state.set('results', ResultsRecord({ @@ -76,15 +77,17 @@ const importResults = (state: State, results: APIEntity, searchTerm: string, sea })); state.set('submitted', true); + state.set('next', next); } }); }; -const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string) => { +const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string, next: string | null) => { return state.withMutations(state => { if (state.value === searchTerm) { state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20); state.setIn(['results', `${searchType}Loaded`], true); + state.set('next', next); state.updateIn(['results', searchType], items => { const data = results[searchType]; // Hashtags are a list of maps. Others are IDs. @@ -129,13 +132,13 @@ export default function search(state = ReducerRecord(), action: AnyAction) { case SEARCH_FETCH_REQUEST: return handleSubmitted(state, action.value); case SEARCH_FETCH_SUCCESS: - return importResults(state, action.results, action.searchTerm, action.searchType); + return importResults(state, action.results, action.searchTerm, action.searchType, action.next); case SEARCH_FILTER_SET: return state.set('filter', action.value); case SEARCH_EXPAND_REQUEST: return state.setIn(['results', `${action.searchType}Loaded`], false); case SEARCH_EXPAND_SUCCESS: - return paginateResults(state, action.searchType, action.results, action.searchTerm); + return paginateResults(state, action.searchType, action.results, action.searchTerm, action.next); case SEARCH_ACCOUNT_SET: if (!action.accountId) return state.merge({ results: ResultsRecord(),