pl-fe: migrate favourites list to tanstack query

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-07-02 15:37:48 +02:00
parent 8be1b7e5e5
commit f673710eb4
5 changed files with 18 additions and 325 deletions

View File

@ -1,200 +0,0 @@
import { isLoggedIn } from 'pl-fe/utils/auth';
import { getClient } from '../api';
import { importEntities } from './importer';
import type { PaginatedResponse, Status } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store';
const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST' as const;
const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS' as const;
const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL' as const;
const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST' as const;
const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS' as const;
const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL' as const;
const ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST' as const;
const ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS' as const;
const ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL' as const;
const ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST' as const;
const ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS' as const;
const ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL' as const;
const fetchFavouritedStatuses = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
if (getState().status_lists.favourites?.isLoading) {
return;
}
dispatch(fetchFavouritedStatusesRequest());
return getClient(getState()).myAccount.getFavourites().then(response => {
dispatch(importEntities({ statuses: response.items }));
dispatch(fetchFavouritedStatusesSuccess(response.items, response.next));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
});
};
const fetchFavouritedStatusesRequest = () => ({
type: FAVOURITED_STATUSES_FETCH_REQUEST,
});
const fetchFavouritedStatusesSuccess = (statuses: Array<Status>, next: (() => Promise<PaginatedResponse<Status>>) | null) => ({
type: FAVOURITED_STATUSES_FETCH_SUCCESS,
statuses,
next,
});
const fetchFavouritedStatusesFail = (error: unknown) => ({
type: FAVOURITED_STATUSES_FETCH_FAIL,
error,
});
const expandFavouritedStatuses = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const next = getState().status_lists.favourites?.next || null;
if (next === null || getState().status_lists.favourites?.isLoading) {
return;
}
dispatch(expandFavouritedStatusesRequest());
return next().then(response => {
dispatch(importEntities({ statuses: response.items }));
dispatch(expandFavouritedStatusesSuccess(response.items, response.next));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));
});
};
const expandFavouritedStatusesRequest = () => ({
type: FAVOURITED_STATUSES_EXPAND_REQUEST,
});
const expandFavouritedStatusesSuccess = (statuses: Array<Status>, next: (() => Promise<PaginatedResponse<Status>>) | null) => ({
type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
});
const expandFavouritedStatusesFail = (error: unknown) => ({
type: FAVOURITED_STATUSES_EXPAND_FAIL,
error,
});
const fetchAccountFavouritedStatuses = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
if (getState().status_lists[`favourites:${accountId}`]?.isLoading) {
return;
}
dispatch(fetchAccountFavouritedStatusesRequest(accountId));
return getClient(getState).accounts.getAccountFavourites(accountId).then(response => {
dispatch(importEntities({ statuses: response.items }));
dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.items, response.next));
}).catch(error => {
dispatch(fetchAccountFavouritedStatusesFail(accountId, error));
});
};
const fetchAccountFavouritedStatusesRequest = (accountId: string) => ({
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
accountId,
});
const fetchAccountFavouritedStatusesSuccess = (accountId: string, statuses: Array<Status>, next: (() => Promise<PaginatedResponse<Status>>) | null) => ({
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
accountId,
statuses,
next,
});
const fetchAccountFavouritedStatusesFail = (accountId: string, error: unknown) => ({
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
accountId,
error,
});
const expandAccountFavouritedStatuses = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const next = getState().status_lists[`favourites:${accountId}`]?.next || null;
if (next === null || getState().status_lists[`favourites:${accountId}`]?.isLoading) {
return;
}
dispatch(expandAccountFavouritedStatusesRequest(accountId));
return next().then(response => {
dispatch(importEntities({ statuses: response.items }));
dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.items, response.next));
}).catch(error => {
dispatch(expandAccountFavouritedStatusesFail(accountId, error));
});
};
const expandAccountFavouritedStatusesRequest = (accountId: string) => ({
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
accountId,
});
const expandAccountFavouritedStatusesSuccess = (accountId: string, statuses: Array<Status>, next: (() => Promise<PaginatedResponse<Status>>) | null) => ({
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
accountId,
statuses,
next,
});
const expandAccountFavouritedStatusesFail = (accountId: string, error: unknown) => ({
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
accountId,
error,
});
type FavouritesAction =
ReturnType<typeof fetchFavouritedStatusesRequest>
| ReturnType<typeof fetchFavouritedStatusesSuccess>
| ReturnType<typeof fetchFavouritedStatusesFail>
| ReturnType<typeof expandFavouritedStatusesRequest>
| ReturnType<typeof expandFavouritedStatusesSuccess>
| ReturnType<typeof expandFavouritedStatusesFail>
| ReturnType<typeof fetchAccountFavouritedStatusesRequest>
| ReturnType<typeof fetchAccountFavouritedStatusesSuccess>
| ReturnType<typeof fetchAccountFavouritedStatusesFail>
| ReturnType<typeof expandAccountFavouritedStatusesRequest>
| ReturnType<typeof expandAccountFavouritedStatusesSuccess>
| ReturnType<typeof expandAccountFavouritedStatusesFail>;
export {
FAVOURITED_STATUSES_FETCH_REQUEST,
FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_FETCH_FAIL,
FAVOURITED_STATUSES_EXPAND_REQUEST,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
FAVOURITED_STATUSES_EXPAND_FAIL,
ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
fetchFavouritedStatuses,
expandFavouritedStatuses,
fetchAccountFavouritedStatuses,
expandAccountFavouritedStatuses,
type FavouritesAction,
};

View File

@ -1,16 +1,12 @@
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchAccount, fetchAccountByUsername } from 'pl-fe/actions/accounts';
import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from 'pl-fe/actions/favourites';
import { useAccountLookup } from 'pl-fe/api/hooks/accounts/use-account-lookup';
import MissingIndicator from 'pl-fe/components/missing-indicator';
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 { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { useFavourites } from 'pl-fe/queries/status-lists/use-favourites';
const messages = defineMessages({
heading: { id: 'column.favourited_statuses', defaultMessage: 'Liked posts' },
@ -25,45 +21,14 @@ interface IFavourites {
/** Timeline displaying a user's favourited statuses. */
const FavouritedStatusesPage: React.FC<IFavourites> = ({ params }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { account: ownAccount } = useOwnAccount();
const { account, isUnavailable } = useAccountLookup(params?.username, { withRelationship: true });
const username = params?.username || '';
const isOwnAccount = username.toLowerCase() === ownAccount?.acct?.toLowerCase();
const accountId = isOwnAccount ? undefined : account?.id;
const timelineKey = isOwnAccount ? 'favourites' : `favourites:${account?.id}`;
const statusIds = useAppSelector(state => state.status_lists[timelineKey]?.items || []);
const isLoading = useAppSelector(state => state.status_lists[timelineKey]?.isLoading === true);
const hasMore = useAppSelector(state => !!state.status_lists[timelineKey]?.next);
const handleLoadMore = useCallback(debounce(() => {
if (isOwnAccount) {
dispatch(expandFavouritedStatuses());
} else if (account) {
dispatch(expandAccountFavouritedStatuses(account.id));
}
}, 300, { leading: true }), [account?.id]);
useEffect(() => {
if (isOwnAccount)
dispatch(fetchFavouritedStatuses());
else {
if (account) {
dispatch(fetchAccount(account.id));
dispatch(fetchAccountFavouritedStatuses(account.id));
} else {
dispatch(fetchAccountByUsername(username));
}
}
}, []);
useEffect(() => {
if (account && !isOwnAccount) {
dispatch(fetchAccount(account.id));
dispatch(fetchAccountFavouritedStatuses(account.id));
}
}, [account?.id]);
const { data: statusIds = [], isFetching, hasNextPage, fetchNextPage } = useFavourites(accountId);
if (isUnavailable) {
return (
@ -89,10 +54,10 @@ const FavouritedStatusesPage: React.FC<IFavourites> = ({ params }) => {
<Column label={intl.formatMessage(messages.heading)} withHeader={false} transparent>
<StatusList
statusIds={statusIds}
scrollKey='favourited_statuses'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={handleLoadMore}
scrollKey={`favourited_statuses:${account.id}`}
hasMore={hasNextPage}
isLoading={isFetching}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
emptyMessage={emptyMessage}
/>
</Column>

View File

@ -0,0 +1,9 @@
import { makePaginatedResponseQuery } from 'pl-fe/queries/utils/make-paginated-response-query';
import { minifyStatusList } from 'pl-fe/queries/utils/minify-list';
const useFavourites = makePaginatedResponseQuery(
(accountId?: string) => ['statusLists', 'favourites', accountId],
(client, [accountId]) => (accountId ? client.accounts.getAccountFavourites(accountId) : client.myAccount.getFavourites()).then(minifyStatusList),
);
export { useFavourites };

View File

@ -3,42 +3,12 @@ import reducer from './status-lists';
describe('status_lists reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any).toJS()).toEqual({
favourites: {
next: null,
loaded: false,
isLoading: null,
items: [],
},
bookmarks: {
next: null,
loaded: false,
isLoading: null,
items: [],
},
pins: {
next: null,
loaded: false,
isLoading: null,
items: [],
},
scheduled_statuses: {
next: null,
loaded: false,
isLoading: null,
items: [],
},
joined_events: {
next: null,
loaded: false,
isLoading: null,
items: [],
},
recent_events: {
next: null,
loaded: false,
isLoading: null,
items: [],
},
});
});
});

View File

@ -1,20 +1,5 @@
import { create } from 'mutative';
import {
FAVOURITED_STATUSES_FETCH_REQUEST,
FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_FETCH_FAIL,
FAVOURITED_STATUSES_EXPAND_REQUEST,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
FAVOURITED_STATUSES_EXPAND_FAIL,
ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
type FavouritesAction,
} from 'pl-fe/actions/favourites';
import {
PIN_SUCCESS,
UNPIN_SUCCESS,
@ -23,7 +8,6 @@ import {
import { PINNED_STATUSES_FETCH_SUCCESS, type PinStatusesAction } from 'pl-fe/actions/pin-statuses';
import type { PaginatedResponse, Status } from 'pl-api';
import type { StatusesAction } from 'pl-fe/actions/statuses';
interface StatusList {
next: (() => Promise<PaginatedResponse<Status>>) | null;
@ -50,11 +34,6 @@ const getStatusId = (status: string | Pick<Status, 'id'>) => typeof status === '
const getStatusIds = (statuses: Array<string | Pick<Status, 'id'>> = []) => statuses.map(getStatusId);
const setLoading = (state: State, listType: string, loading: boolean) => {
const list = state[listType] = state[listType] || newStatusList();
list.isLoading = loading;
};
const normalizeList = (state: State, listType: string, statuses: Array<string | Pick<Status, 'id'>>, next: (() => Promise<PaginatedResponse<Status>>) | null) => {
const list = state[listType] = state[listType] || newStatusList();
@ -64,16 +43,6 @@ const normalizeList = (state: State, listType: string, statuses: Array<string |
list.items = getStatusIds(statuses);
};
const appendToList = (state: State, listType: string, statuses: Array<string | Pick<Status, 'id'>>, next: (() => Promise<PaginatedResponse<Status>>) | null) => {
const newIds = getStatusIds(statuses);
const list = state[listType] = state[listType] || newStatusList();
list.next = next;
list.isLoading = false;
list.items = [...new Set([...list.items, ...newIds])];
};
const prependOneToList = (state: State, listType: string, status: string | Pick<Status, 'id'>) => {
const statusId = getStatusId(status);
const list = state[listType] = state[listType] || newStatusList();
@ -88,28 +57,8 @@ const removeOneFromList = (state: State, listType: string, status: string | Pick
list.items = list.items.filter(id => id !== statusId);
};
const statusLists = (state = initialState, action: FavouritesAction | InteractionsAction | PinStatusesAction | StatusesAction): State => {
const statusLists = (state = initialState, action: InteractionsAction | PinStatusesAction): State => {
switch (action.type) {
case FAVOURITED_STATUSES_FETCH_REQUEST:
case FAVOURITED_STATUSES_EXPAND_REQUEST:
return create(state, draft => setLoading(draft, 'favourites', true));
case FAVOURITED_STATUSES_FETCH_FAIL:
case FAVOURITED_STATUSES_EXPAND_FAIL:
return create(state, draft => setLoading(draft, 'favourites', false));
case FAVOURITED_STATUSES_FETCH_SUCCESS:
return create(state, draft => normalizeList(draft, 'favourites', action.statuses, action.next));
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
return create(state, draft => appendToList(draft, 'favourites', action.statuses, action.next));
case ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST:
return create(state, draft => setLoading(draft, `favourites:${action.accountId}`, true));
case ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL:
return create(state, draft => setLoading(draft, `favourites:${action.accountId}`, false));
case ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS:
return create(state, draft => normalizeList(draft, `favourites:${action.accountId}`, action.statuses, action.next));
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS:
return create(state, draft => appendToList(draft, `favourites:${action.accountId}`, action.statuses, action.next));
case PINNED_STATUSES_FETCH_SUCCESS:
return create(state, draft => normalizeList(draft, 'pins', action.statuses, action.next));
case PIN_SUCCESS: