Merge branch 'next-paginated-context' into 'next'

Next: Support paginated context API

See merge request soapbox-pub/soapbox-fe!1253
This commit is contained in:
Alex Gleason 2022-04-24 17:08:19 +00:00
commit 3b55a5a9c7
4 changed files with 76 additions and 11 deletions

View File

@ -2,7 +2,7 @@ import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion } from 'soapbox/utils/features'; import { getFeatures, parseVersion } from 'soapbox/utils/features';
import { shouldHaveCard } from 'soapbox/utils/status'; import { shouldHaveCard } from 'soapbox/utils/status';
import api from '../api'; import api, { getNextLink } from '../api';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import { openModal } from './modals'; 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) { export function fetchStatusWithContext(id) {
return (dispatch, getState) => { return async(dispatch, getState) => {
return Promise.all([ 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(fetchContext(id)),
dispatch(fetchStatus(id)), dispatch(fetchStatus(id)),
]); ]);
return { next: undefined };
}
}; };
} }

View File

@ -24,6 +24,10 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
return new LinkHeader(response.headers?.link); 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) => { const getToken = (state: RootState, authType: string) => {
return authType === 'app' ? getAppToken(state) : getAccessToken(state); return authType === 'app' ? getAppToken(state) : getAccessToken(state);
}; };

View File

@ -51,7 +51,7 @@ import {
hideStatus, hideStatus,
revealStatus, revealStatus,
} from '../../actions/statuses'; } from '../../actions/statuses';
import { fetchStatusWithContext } from '../../actions/statuses'; import { fetchStatusWithContext, fetchNext } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator'; import MissingIndicator from '../../components/missing_indicator';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import { makeGetStatus } from '../../selectors'; import { makeGetStatus } from '../../selectors';
@ -189,6 +189,7 @@ interface IStatusState {
emojiSelectorFocused: boolean, emojiSelectorFocused: boolean,
isLoaded: boolean, isLoaded: boolean,
error?: AxiosError, error?: AxiosError,
next?: string,
} }
class Status extends ImmutablePureComponent<IStatus, IStatusState> { class Status extends ImmutablePureComponent<IStatus, IStatusState> {
@ -200,17 +201,18 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
emojiSelectorFocused: false, emojiSelectorFocused: false,
isLoaded: Boolean(this.props.status), isLoaded: Boolean(this.props.status),
error: undefined, error: undefined,
next: undefined,
}; };
node: HTMLDivElement | null = null; node: HTMLDivElement | null = null;
status: HTMLDivElement | null = null; status: HTMLDivElement | null = null;
_scrolledIntoView: boolean = false; _scrolledIntoView: boolean = false;
fetchData = () => { fetchData = async() => {
const { dispatch, params } = this.props; const { dispatch, params } = this.props;
const { statusId } = params; const { statusId } = params;
const { next } = await dispatch(fetchStatusWithContext(statusId));
return dispatch(fetchStatusWithContext(statusId)); this.setState({ next });
} }
componentDidMount() { componentDidMount() {
@ -641,6 +643,16 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
return this.fetchData(); return this.fetchData();
} }
handleLoadMore = () => {
const { next } = this.state;
if (next) {
this.props.dispatch(fetchNext(next)).then(({ next }) => {
this.setState({ next });
}).catch(() => {});
}
}
render() { render() {
const { status, ancestorsIds, descendantsIds, intl } = this.props; const { status, ancestorsIds, descendantsIds, intl } = this.props;
@ -754,7 +766,12 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
</div> </div>
<div ref={this.setRef} className='thread'> <div ref={this.setRef} className='thread'>
<ScrollableList onRefresh={this.handleRefresh}> <ScrollableList
onRefresh={this.handleRefresh}
hasMore={!!this.state.next}
onLoadMore={this.handleLoadMore}
placeholderComponent={() => <PlaceholderStatus thread />}
>
{children} {children}
</ScrollableList> </ScrollableList>
</div> </div>

View File

@ -286,6 +286,13 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA && gte(v.version, '2.4.50'), 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. */ /** Truth Social account registration API. */
pepe: v.software === TRUTHSOCIAL, pepe: v.software === TRUTHSOCIAL,