diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 3433f47f2..cbb1c6ed2 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -2,7 +2,7 @@ import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion } from 'soapbox/utils/features'; import { shouldHaveCard } from 'soapbox/utils/status'; -import api from '../api'; +import api, { getNextLink } from '../api'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { openModal } from './modals'; @@ -167,12 +167,49 @@ export function fetchContext(id) { }; } +export function fetchNext(next) { + return async(dispatch, getState) => { + const response = await api(getState).get(next); + dispatch(importFetchedStatuses(response.data)); + return { next: getNextLink(response) }; + }; +} + +export function fetchAncestors(id) { + return async(dispatch, getState) => { + const response = await api(getState).get(`/api/v1/statuses/${id}/context/ancestors`); + dispatch(importFetchedStatuses(response.data)); + return response; + }; +} + +export function fetchDescendants(id) { + return async(dispatch, getState) => { + const response = await api(getState).get(`/api/v1/statuses/${id}/context/descendants`); + dispatch(importFetchedStatuses(response.data)); + return response; + }; +} + export function fetchStatusWithContext(id) { - return (dispatch, getState) => { - return Promise.all([ - dispatch(fetchContext(id)), - dispatch(fetchStatus(id)), - ]); + return async(dispatch, getState) => { + const features = getFeatures(getState().instance); + + if (features.paginatedContext) { + const responses = await Promise.all([ + dispatch(fetchAncestors(id)), + dispatch(fetchDescendants(id)), + dispatch(fetchStatus(id)), + ]); + const next = getNextLink(responses[1]); + return { next }; + } else { + await Promise.all([ + dispatch(fetchContext(id)), + dispatch(fetchStatus(id)), + ]); + return { next: undefined }; + } }; } diff --git a/app/soapbox/api.ts b/app/soapbox/api.ts index 6c81042bb..34ed699f8 100644 --- a/app/soapbox/api.ts +++ b/app/soapbox/api.ts @@ -24,6 +24,10 @@ export const getLinks = (response: AxiosResponse): LinkHeader => { return new LinkHeader(response.headers?.link); }; +export const getNextLink = (response: AxiosResponse): string | undefined => { + return getLinks(response).refs.find(link => link.rel === 'next')?.uri; +}; + const getToken = (state: RootState, authType: string) => { return authType === 'app' ? getAppToken(state) : getAccessToken(state); }; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 2c8e1a75c..9795bcb8d 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -51,7 +51,7 @@ import { hideStatus, revealStatus, } from '../../actions/statuses'; -import { fetchStatusWithContext } from '../../actions/statuses'; +import { fetchStatusWithContext, fetchNext } from '../../actions/statuses'; import MissingIndicator from '../../components/missing_indicator'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { makeGetStatus } from '../../selectors'; @@ -189,6 +189,7 @@ interface IStatusState { emojiSelectorFocused: boolean, isLoaded: boolean, error?: AxiosError, + next?: string, } class Status extends ImmutablePureComponent { @@ -200,17 +201,18 @@ class Status extends ImmutablePureComponent { emojiSelectorFocused: false, isLoaded: Boolean(this.props.status), error: undefined, + next: undefined, }; node: HTMLDivElement | null = null; status: HTMLDivElement | null = null; _scrolledIntoView: boolean = false; - fetchData = () => { + fetchData = async() => { const { dispatch, params } = this.props; const { statusId } = params; - - return dispatch(fetchStatusWithContext(statusId)); + const { next } = await dispatch(fetchStatusWithContext(statusId)); + this.setState({ next }); } componentDidMount() { @@ -641,6 +643,16 @@ class Status extends ImmutablePureComponent { return this.fetchData(); } + handleLoadMore = () => { + const { next } = this.state; + + if (next) { + this.props.dispatch(fetchNext(next)).then(({ next }) => { + this.setState({ next }); + }).catch(() => {}); + } + } + render() { const { status, ancestorsIds, descendantsIds, intl } = this.props; @@ -754,7 +766,11 @@ class Status extends ImmutablePureComponent {
- + {children}
diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 947dc4d12..9864e8889 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -286,6 +286,13 @@ const getInstanceFeatures = (instance: Instance) => { v.software === PLEROMA && gte(v.version, '2.4.50'), ]), + /** + * Supports pagination in threads. + * @see GET /api/v1/statuses/:id/context/ancestors + * @see GET /api/v1/statuses/:id/context/descendants + */ + paginatedContext: v.software === TRUTHSOCIAL, + /** Truth Social account registration API. */ pepe: v.software === TRUTHSOCIAL,