From 2bc6ff3fa3d70f3778970412513185fb6dfe3aab Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 2 Apr 2023 18:32:50 -0500 Subject: [PATCH] Use Link header for home timeline pagination --- app/soapbox/actions/timelines.ts | 56 ++++++++++++++++--- .../features/feed-filtering/feed-carousel.tsx | 2 +- app/soapbox/features/home-timeline/index.tsx | 5 +- app/soapbox/features/test-timeline/index.tsx | 2 +- app/soapbox/reducers/timelines.ts | 24 +++++++- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index 7ae023338..cafaa6aa5 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -4,7 +4,7 @@ import { getSettings } from 'soapbox/actions/settings'; import { normalizeStatus } from 'soapbox/normalizers'; import { shouldFilter } from 'soapbox/utils/timelines'; -import api, { getLinks } from '../api'; +import api, { getNextLink, getPrevLink } from '../api'; import { importFetchedStatus, importFetchedStatuses } from './importer'; @@ -139,7 +139,7 @@ const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none }; const replaceHomeTimeline = ( - accountId: string | null, + accountId: string | undefined, { maxId }: Record = {}, done?: () => void, ) => (dispatch: AppDispatch, _getState: () => RootState) => { @@ -162,7 +162,12 @@ const expandTimeline = (timelineId: string, path: string, params: Record 0) { + if ( + !params.max_id && + !params.pinned && + (timeline.items || ImmutableOrderedSet()).size > 0 && + !path.includes('max_id=') + ) { params.since_id = timeline.getIn(['items', 0]); } @@ -171,9 +176,16 @@ const expandTimeline = (timelineId: string, path: string, params: Record { - const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess( + timelineId, + response.data, + getNextLink(response), + getPrevLink(response), + response.status === 206, + isLoadingRecent, + isLoadingMore, + )); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -181,9 +193,26 @@ const expandTimeline = (timelineId: string, path: string, params: Record = {}, done = noOp) => { - const endpoint = accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home'; - const params: any = { max_id: maxId }; +interface ExpandHomeTimelineOpts { + accountId?: string + maxId?: string + url?: string +} + +interface HomeTimelineParams { + max_id?: string + exclude_replies?: boolean + with_muted?: boolean +} + +const expandHomeTimeline = ({ url, accountId, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => { + const endpoint = url || (accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home'); + const params: HomeTimelineParams = {}; + + if (!url && maxId) { + params.max_id = maxId; + } + if (accountId) { params.exclude_replies = true; params.with_muted = true; @@ -237,11 +266,20 @@ const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({ skipLoading: !isLoadingMore, }); -const expandTimelineSuccess = (timeline: string, statuses: APIEntity[], next: string | null, partial: boolean, isLoadingRecent: boolean, isLoadingMore: boolean) => ({ +const expandTimelineSuccess = ( + timeline: string, + statuses: APIEntity[], + next: string | undefined, + prev: string | undefined, + partial: boolean, + isLoadingRecent: boolean, + isLoadingMore: boolean, +) => ({ type: TIMELINE_EXPAND_SUCCESS, timeline, statuses, next, + prev, partial, isLoadingRecent, skipLoading: !isLoadingMore, diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index af6cb47bb..9d62538ce 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -30,7 +30,7 @@ const CarouselItem = React.forwardRef(( setLoading(true); if (isSelected) { - dispatch(replaceHomeTimeline(null, { maxId: null }, () => setLoading(false))); + dispatch(replaceHomeTimeline(undefined, { maxId: null }, () => setLoading(false))); if (onPinned) { onPinned(null); diff --git a/app/soapbox/features/home-timeline/index.tsx b/app/soapbox/features/home-timeline/index.tsx index aaaec2bb3..611fcf7ce 100644 --- a/app/soapbox/features/home-timeline/index.tsx +++ b/app/soapbox/features/home-timeline/index.tsx @@ -27,9 +27,10 @@ const HomeTimeline: React.FC = () => { const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true); const currentAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId as string | undefined); const currentAccountRelationship = useAppSelector(state => currentAccountId ? state.relationships.get(currentAccountId) : null); + const next = useAppSelector(state => state.timelines.get('home')?.next); const handleLoadMore = (maxId: string) => { - dispatch(expandHomeTimeline({ maxId, accountId: currentAccountId })); + dispatch(expandHomeTimeline({ url: next, maxId, accountId: currentAccountId })); }; // Mastodon generates the feed in Redis, and can return a partial timeline @@ -52,7 +53,7 @@ const HomeTimeline: React.FC = () => { }; const handleRefresh = () => { - return dispatch(expandHomeTimeline({ maxId: null, accountId: currentAccountId })); + return dispatch(expandHomeTimeline({ accountId: currentAccountId })); }; useEffect(() => { diff --git a/app/soapbox/features/test-timeline/index.tsx b/app/soapbox/features/test-timeline/index.tsx index 51ab1491e..136d0d324 100644 --- a/app/soapbox/features/test-timeline/index.tsx +++ b/app/soapbox/features/test-timeline/index.tsx @@ -35,7 +35,7 @@ const TestTimeline: React.FC = () => { React.useEffect(() => { dispatch(importFetchedStatuses(MOCK_STATUSES)); - dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, null, false, false, false)); + dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, undefined, undefined, false, false, false)); }, []); return ( diff --git a/app/soapbox/reducers/timelines.ts b/app/soapbox/reducers/timelines.ts index 1713ee964..b5fe0e049 100644 --- a/app/soapbox/reducers/timelines.ts +++ b/app/soapbox/reducers/timelines.ts @@ -46,6 +46,8 @@ const TimelineRecord = ImmutableRecord({ top: true, isLoading: false, hasMore: true, + next: undefined as string | undefined, + prev: undefined as string | undefined, items: ImmutableOrderedSet(), queuedItems: ImmutableOrderedSet(), //max= MAX_QUEUED_ITEMS feedAccountId: null, @@ -87,13 +89,23 @@ const setFailed = (state: State, timelineId: string, failed: boolean) => { return state.update(timelineId, TimelineRecord(), timeline => timeline.set('loadingFailed', failed)); }; -const expandNormalizedTimeline = (state: State, timelineId: string, statuses: ImmutableList>, next: string | null, isPartial: boolean, isLoadingRecent: boolean) => { +const expandNormalizedTimeline = ( + state: State, + timelineId: string, + statuses: ImmutableList>, + next: string | undefined, + prev: string | undefined, + isPartial: boolean, + isLoadingRecent: boolean, +) => { const newIds = getStatusIds(statuses); return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => { timeline.set('isLoading', false); timeline.set('loadingFailed', false); timeline.set('isPartial', isPartial); + timeline.set('next', next); + timeline.set('prev', prev); if (!next && !isLoadingRecent) timeline.set('hasMore', false); @@ -322,7 +334,15 @@ export default function timelines(state: State = initialState, action: AnyAction case TIMELINE_EXPAND_FAIL: return handleExpandFail(state, action.timeline); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses) as ImmutableList>, action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline( + state, + action.timeline, + fromJS(action.statuses) as ImmutableList>, + action.next, + action.prev, + action.partial, + action.isLoadingRecent, + ); case TIMELINE_UPDATE: return updateTimeline(state, action.timeline, action.statusId); case TIMELINE_UPDATE_QUEUE: