pl-fe: migrate status quotes list to tanstack query
Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
parent
d95a78085e
commit
a99c61d454
7 changed files with 43 additions and 322 deletions
|
@ -1,157 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'pl-fe/api';
|
||||
import { mockStore, rootState } from 'pl-fe/jest/test-helpers';
|
||||
import { StatusListRecord } from 'pl-fe/reducers/status-lists';
|
||||
|
||||
import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes';
|
||||
|
||||
const status = {
|
||||
account: {
|
||||
id: 'ABDSjI3Q0R8aDaz1U0',
|
||||
},
|
||||
content: 'quoast',
|
||||
id: 'AJsajx9hY4Q7IKQXEe',
|
||||
pleroma: {
|
||||
quote: {
|
||||
content: '<p>10</p>',
|
||||
id: 'AJmoVikzI3SkyITyim',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const statusId = 'AJmoVikzI3SkyITyim';
|
||||
|
||||
describe('fetchStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: '1234' };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(async () => {
|
||||
const quotes = await import('pl-fe/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status] },
|
||||
{ type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('without a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
me: '1234',
|
||||
status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }),
|
||||
};
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }),
|
||||
me: '1234',
|
||||
};
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(async () => {
|
||||
const quotes = await import('pl-fe/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status] },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,127 +0,0 @@
|
|||
import { getClient } from '../api';
|
||||
|
||||
import { importEntities } from './importer';
|
||||
|
||||
import type { Status as BaseStatus, PaginatedResponse } from 'pl-api';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const STATUS_QUOTES_FETCH_REQUEST = 'STATUS_QUOTES_FETCH_REQUEST' as const;
|
||||
const STATUS_QUOTES_FETCH_SUCCESS = 'STATUS_QUOTES_FETCH_SUCCESS' as const;
|
||||
const STATUS_QUOTES_FETCH_FAIL = 'STATUS_QUOTES_FETCH_FAIL' as const;
|
||||
|
||||
const STATUS_QUOTES_EXPAND_REQUEST = 'STATUS_QUOTES_EXPAND_REQUEST' as const;
|
||||
const STATUS_QUOTES_EXPAND_SUCCESS = 'STATUS_QUOTES_EXPAND_SUCCESS' as const;
|
||||
const STATUS_QUOTES_EXPAND_FAIL = 'STATUS_QUOTES_EXPAND_FAIL' as const;
|
||||
|
||||
const noOp = () => new Promise(f => f(null));
|
||||
|
||||
interface FetchStatusQuotesRequestAction {
|
||||
type: typeof STATUS_QUOTES_FETCH_REQUEST;
|
||||
statusId: string;
|
||||
}
|
||||
|
||||
interface FetchStatusQuotesSuccessAction {
|
||||
type: typeof STATUS_QUOTES_FETCH_SUCCESS;
|
||||
statusId: string;
|
||||
statuses: Array<BaseStatus>;
|
||||
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null;
|
||||
}
|
||||
|
||||
interface FetchStatusQuotesFailAction {
|
||||
type: typeof STATUS_QUOTES_FETCH_FAIL;
|
||||
statusId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
const fetchStatusQuotes = (statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (getState().status_lists[`quotes:${statusId}`]?.isLoading) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch<FetchStatusQuotesRequestAction>({ type: STATUS_QUOTES_FETCH_REQUEST, statusId });
|
||||
|
||||
return getClient(getState).statuses.getStatusQuotes(statusId).then(response => {
|
||||
dispatch(importEntities({ statuses: response.items }));
|
||||
return dispatch<FetchStatusQuotesSuccessAction>({
|
||||
type: STATUS_QUOTES_FETCH_SUCCESS,
|
||||
statusId,
|
||||
statuses: response.items,
|
||||
next: response.next,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch<FetchStatusQuotesFailAction>({
|
||||
type: STATUS_QUOTES_FETCH_FAIL,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
interface ExpandStatusQuotesRequestAction {
|
||||
type: typeof STATUS_QUOTES_EXPAND_REQUEST;
|
||||
statusId: string;
|
||||
}
|
||||
|
||||
interface ExpandStatusQuotesSuccessAction {
|
||||
type: typeof STATUS_QUOTES_EXPAND_SUCCESS;
|
||||
statusId: string;
|
||||
statuses: Array<BaseStatus>;
|
||||
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null;
|
||||
}
|
||||
|
||||
interface ExpandStatusQuotesFailAction {
|
||||
type: typeof STATUS_QUOTES_EXPAND_FAIL;
|
||||
statusId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
const expandStatusQuotes = (statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const next = getState().status_lists[`quotes:${statusId}`]?.next || null;
|
||||
|
||||
if (next === null || getState().status_lists[`quotes:${statusId}`]?.isLoading) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch<ExpandStatusQuotesRequestAction>({
|
||||
type: STATUS_QUOTES_EXPAND_REQUEST,
|
||||
statusId,
|
||||
});
|
||||
|
||||
return next().then(response => {
|
||||
dispatch(importEntities({ statuses: response.items }));
|
||||
dispatch<ExpandStatusQuotesSuccessAction>({
|
||||
type: STATUS_QUOTES_EXPAND_SUCCESS,
|
||||
statusId,
|
||||
statuses: response.items,
|
||||
next: response.next,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch<ExpandStatusQuotesFailAction>({
|
||||
type: STATUS_QUOTES_EXPAND_FAIL,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
type StatusQuotesAction =
|
||||
| FetchStatusQuotesRequestAction
|
||||
| FetchStatusQuotesSuccessAction
|
||||
| FetchStatusQuotesFailAction
|
||||
| ExpandStatusQuotesRequestAction
|
||||
| ExpandStatusQuotesSuccessAction
|
||||
| ExpandStatusQuotesFailAction;
|
||||
|
||||
export {
|
||||
STATUS_QUOTES_FETCH_REQUEST,
|
||||
STATUS_QUOTES_FETCH_SUCCESS,
|
||||
STATUS_QUOTES_FETCH_FAIL,
|
||||
STATUS_QUOTES_EXPAND_REQUEST,
|
||||
STATUS_QUOTES_EXPAND_SUCCESS,
|
||||
STATUS_QUOTES_EXPAND_FAIL,
|
||||
fetchStatusQuotes,
|
||||
expandStatusQuotes,
|
||||
type StatusQuotesAction,
|
||||
};
|
20
packages/pl-fe/src/api/hooks/statuses/use-status-quotes.ts
Normal file
20
packages/pl-fe/src/api/hooks/statuses/use-status-quotes.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import { minifyStatusList } from 'pl-fe/api/normalizers/status-list';
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
|
||||
import type { PaginatedResponse } from 'pl-api';
|
||||
|
||||
const useStatusQuotes = (statusId: string) => {
|
||||
const client = useClient();
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['statusLists', 'quotes', statusId],
|
||||
queryFn: ({ pageParam }) => pageParam.next?.() || client.statuses.getStatusQuotes(statusId).then(minifyStatusList),
|
||||
initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse<string>,
|
||||
getNextPageParam: (page) => page.next ? page : undefined,
|
||||
select: (data) => data.pages.map(page => page.items).flat(),
|
||||
});
|
||||
};
|
||||
|
||||
export { useStatusQuotes };
|
17
packages/pl-fe/src/api/normalizers/status-list.ts
Normal file
17
packages/pl-fe/src/api/normalizers/status-list.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { PaginatedResponse, Status } from 'pl-api';
|
||||
|
||||
import { importEntities } from 'pl-fe/actions/importer';
|
||||
import { store } from 'pl-fe/store';
|
||||
|
||||
const minifyStatusList = ({ previous, next, items, ...response }: PaginatedResponse<Status>): PaginatedResponse<string> => {
|
||||
store.dispatch(importEntities({ statuses: items }) as any);
|
||||
|
||||
return {
|
||||
...response,
|
||||
previous: previous ? () => previous().then(minifyStatusList) : null,
|
||||
next: next ? () => next().then(minifyStatusList) : null,
|
||||
items: items.map(status => status.id),
|
||||
};
|
||||
};
|
||||
|
||||
export { minifyStatusList };
|
|
@ -1,13 +1,10 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { expandStatusQuotes, fetchStatusQuotes } from 'pl-fe/actions/status-quotes';
|
||||
import { useStatusQuotes } from 'pl-fe/api/hooks/statuses/use-status-quotes';
|
||||
import StatusList from 'pl-fe/components/status-list';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useIsMobile } from 'pl-fe/hooks/use-is-mobile';
|
||||
import { useTheme } from 'pl-fe/hooks/use-theme';
|
||||
|
||||
|
@ -15,23 +12,13 @@ const messages = defineMessages({
|
|||
heading: { id: 'column.quotes', defaultMessage: 'Post quotes' },
|
||||
});
|
||||
|
||||
const handleLoadMore = debounce((statusId: string, dispatch: React.Dispatch<any>) =>
|
||||
dispatch(expandStatusQuotes(statusId)), 300, { leading: true });
|
||||
|
||||
const Quotes: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const { statusId } = useParams<{ statusId: string }>();
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const statusIds = useAppSelector((state) => state.status_lists[`quotes:${statusId}`]?.items || []);
|
||||
const isLoading = useAppSelector((state) => state.status_lists[`quotes:${statusId}`]?.isLoading !== false);
|
||||
const hasMore = useAppSelector((state) => !!state.status_lists[`quotes:${statusId}`]?.next);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchStatusQuotes(statusId));
|
||||
}, [statusId]);
|
||||
const { data: statusIds = [], isLoading, hasNextPage, fetchNextPage } = useStatusQuotes(statusId);
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.quotes' defaultMessage='This post has not been quoted yet.' />;
|
||||
|
||||
|
@ -42,9 +29,9 @@ const Quotes: React.FC = () => {
|
|||
loadMoreClassName='black:sm:mx-4'
|
||||
statusIds={statusIds}
|
||||
scrollKey={`quotes:${statusId}`}
|
||||
hasMore={hasMore}
|
||||
hasMore={hasNextPage}
|
||||
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
|
||||
onLoadMore={() => handleLoadMore(statusId, dispatch)}
|
||||
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
|
||||
emptyMessage={emptyMessage}
|
||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
||||
/>
|
||||
|
|
|
@ -54,15 +54,6 @@ import {
|
|||
SCHEDULED_STATUS_CANCEL_SUCCESS,
|
||||
type ScheduledStatusesAction,
|
||||
} from 'pl-fe/actions/scheduled-statuses';
|
||||
import {
|
||||
STATUS_QUOTES_EXPAND_FAIL,
|
||||
STATUS_QUOTES_EXPAND_REQUEST,
|
||||
STATUS_QUOTES_EXPAND_SUCCESS,
|
||||
STATUS_QUOTES_FETCH_FAIL,
|
||||
STATUS_QUOTES_FETCH_REQUEST,
|
||||
STATUS_QUOTES_FETCH_SUCCESS,
|
||||
type StatusQuotesAction,
|
||||
} from 'pl-fe/actions/status-quotes';
|
||||
import { STATUS_CREATE_SUCCESS, type StatusesAction } from 'pl-fe/actions/statuses';
|
||||
|
||||
import type { PaginatedResponse, ScheduledStatus, Status } from 'pl-api';
|
||||
|
@ -155,7 +146,7 @@ const removeBookmarkFromLists = (state: State, status: Pick<Status, 'id' | 'book
|
|||
}
|
||||
};
|
||||
|
||||
const statusLists = (state = initialState, action: BookmarksAction | EventsAction | FavouritesAction | InteractionsAction | PinStatusesAction | ScheduledStatusesAction | StatusesAction | StatusQuotesAction): State => {
|
||||
const statusLists = (state = initialState, action: BookmarksAction | EventsAction | FavouritesAction | InteractionsAction | PinStatusesAction | ScheduledStatusesAction | StatusesAction): State => {
|
||||
switch (action.type) {
|
||||
case FAVOURITED_STATUSES_FETCH_REQUEST:
|
||||
case FAVOURITED_STATUSES_EXPAND_REQUEST:
|
||||
|
@ -214,16 +205,6 @@ const statusLists = (state = initialState, action: BookmarksAction | EventsActio
|
|||
case SCHEDULED_STATUS_CANCEL_REQUEST:
|
||||
case SCHEDULED_STATUS_CANCEL_SUCCESS:
|
||||
return create(state, draft => removeOneFromList(draft, 'scheduled_statuses', action.statusId));
|
||||
case STATUS_QUOTES_FETCH_REQUEST:
|
||||
case STATUS_QUOTES_EXPAND_REQUEST:
|
||||
return create(state, draft => setLoading(draft, `quotes:${action.statusId}`, true));
|
||||
case STATUS_QUOTES_FETCH_FAIL:
|
||||
case STATUS_QUOTES_EXPAND_FAIL:
|
||||
return create(state, draft => setLoading(draft, `quotes:${action.statusId}`, false));
|
||||
case STATUS_QUOTES_FETCH_SUCCESS:
|
||||
return create(state, draft => normalizeList(draft, `quotes:${action.statusId}`, action.statuses, action.next));
|
||||
case STATUS_QUOTES_EXPAND_SUCCESS:
|
||||
return create(state, draft => appendToList(draft, `quotes:${action.statusId}`, action.statuses, action.next));
|
||||
case RECENT_EVENTS_FETCH_REQUEST:
|
||||
return create(state, draft => setLoading(draft, 'recent_events', true));
|
||||
case RECENT_EVENTS_FETCH_FAIL:
|
||||
|
|
|
@ -11,7 +11,7 @@ const useStatusQuotes = (statusId: string) => {
|
|||
const { client } = usePlHooksApiClient();
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['statusesLists', 'quotes', statusId],
|
||||
queryKey: ['statusLists', 'quotes', statusId],
|
||||
queryFn: ({ pageParam }) => pageParam.next?.() || client.statuses.getStatusQuotes(statusId).then(minifyStatusList),
|
||||
initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse<string>,
|
||||
getNextPageParam: (page) => page.next ? page : undefined,
|
||||
|
|
Loading…
Reference in a new issue