diff --git a/packages/pl-fe/src/actions/status-quotes.test.ts b/packages/pl-fe/src/actions/status-quotes.test.ts
deleted file mode 100644
index 13a030534..000000000
--- a/packages/pl-fe/src/actions/status-quotes.test.ts
+++ /dev/null
@@ -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: '
10
',
- id: 'AJmoVikzI3SkyITyim',
- },
- },
-};
-
-const statusId = 'AJmoVikzI3SkyITyim';
-
-describe('fetchStatusQuotes()', () => {
- let store: ReturnType;
-
- 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: `; 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;
-
- 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: `; 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);
- });
- });
- });
-});
diff --git a/packages/pl-fe/src/actions/status-quotes.ts b/packages/pl-fe/src/actions/status-quotes.ts
deleted file mode 100644
index d5f0251b0..000000000
--- a/packages/pl-fe/src/actions/status-quotes.ts
+++ /dev/null
@@ -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;
- next: (() => Promise>) | 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({ type: STATUS_QUOTES_FETCH_REQUEST, statusId });
-
- return getClient(getState).statuses.getStatusQuotes(statusId).then(response => {
- dispatch(importEntities({ statuses: response.items }));
- return dispatch({
- type: STATUS_QUOTES_FETCH_SUCCESS,
- statusId,
- statuses: response.items,
- next: response.next,
- });
- }).catch(error => {
- dispatch({
- 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;
- next: (() => Promise>) | 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({
- type: STATUS_QUOTES_EXPAND_REQUEST,
- statusId,
- });
-
- return next().then(response => {
- dispatch(importEntities({ statuses: response.items }));
- dispatch({
- type: STATUS_QUOTES_EXPAND_SUCCESS,
- statusId,
- statuses: response.items,
- next: response.next,
- });
- }).catch(error => {
- dispatch({
- 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,
-};
diff --git a/packages/pl-fe/src/api/hooks/statuses/use-status-quotes.ts b/packages/pl-fe/src/api/hooks/statuses/use-status-quotes.ts
new file mode 100644
index 000000000..0fc0d9edd
--- /dev/null
+++ b/packages/pl-fe/src/api/hooks/statuses/use-status-quotes.ts
@@ -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,
+ getNextPageParam: (page) => page.next ? page : undefined,
+ select: (data) => data.pages.map(page => page.items).flat(),
+ });
+};
+
+export { useStatusQuotes };
diff --git a/packages/pl-fe/src/api/normalizers/status-list.ts b/packages/pl-fe/src/api/normalizers/status-list.ts
new file mode 100644
index 000000000..9e8333462
--- /dev/null
+++ b/packages/pl-fe/src/api/normalizers/status-list.ts
@@ -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): PaginatedResponse => {
+ 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 };
diff --git a/packages/pl-fe/src/features/quotes/index.tsx b/packages/pl-fe/src/features/quotes/index.tsx
index ecfdef70e..8560c31d9 100644
--- a/packages/pl-fe/src/features/quotes/index.tsx
+++ b/packages/pl-fe/src/features/quotes/index.tsx
@@ -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) =>
- 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 = ;
@@ -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'}
/>
diff --git a/packages/pl-fe/src/reducers/status-lists.ts b/packages/pl-fe/src/reducers/status-lists.ts
index 0bf038b6f..bdf663edc 100644
--- a/packages/pl-fe/src/reducers/status-lists.ts
+++ b/packages/pl-fe/src/reducers/status-lists.ts
@@ -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 {
+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:
diff --git a/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts b/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts
index 972a4e3df..1bf126e53 100644
--- a/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts
+++ b/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts
@@ -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,
getNextPageParam: (page) => page.next ? page : undefined,