pl-fe: migrate suggested accounts to react query

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-10 21:01:44 +01:00
parent 781a2430e4
commit 8f8bd724cb
11 changed files with 63 additions and 158 deletions

View file

@ -1,6 +1,16 @@
import { PLEROMA, type UpdateNotificationSettingsParams, type Account, type CreateAccountParams, type PaginatedResponse, type Relationship, Token, PlApiClient } from 'pl-api';
import {
PLEROMA,
type UpdateNotificationSettingsParams,
type Account,
type CreateAccountParams,
type PaginatedResponse,
type PlApiClient,
type Relationship,
type Token,
} from 'pl-api';
import { Entities } from 'pl-fe/entity-store/entities';
import { queryClient } from 'pl-fe/queries/client';
import { selectAccount } from 'pl-fe/selectors';
import { isLoggedIn } from 'pl-fe/utils/auth';
@ -8,6 +18,7 @@ import { getClient, type PlfeResponse } from '../api';
import { importEntities } from './importer';
import type { MinifiedSuggestion } from 'pl-fe/api/hooks/trends/use-suggested-accounts';
import type { MinifiedStatus } from 'pl-fe/reducers/statuses';
import type { AppDispatch, RootState } from 'pl-fe/store';
import type { History } from 'pl-fe/types/history';
@ -187,6 +198,11 @@ const blockAccount = (accountId: string) =>
return getClient(getState).filtering.blockAccount(accountId)
.then(response => {
dispatch(importEntities({ relationships: [response] }));
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => suggestion.account_id !== accountId)
: undefined);
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch(blockAccountSuccess(response, getState().statuses));
}).catch(error => dispatch(blockAccountFail(error)));
@ -243,6 +259,11 @@ const muteAccount = (accountId: string, notifications?: boolean, duration = 0) =
return client.filtering.muteAccount(accountId, params)
.then(response => {
dispatch(importEntities({ relationships: [response] }));
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => suggestion.account_id !== accountId)
: undefined);
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch(muteAccountSuccess(response, getState().statuses));
})

View file

@ -1,9 +1,11 @@
import { Entities } from 'pl-fe/entity-store/entities';
import { queryClient } from 'pl-fe/queries/client';
import { isLoggedIn } from 'pl-fe/utils/auth';
import { getClient } from '../api';
import type { PaginatedResponse } from 'pl-api';
import type { MinifiedSuggestion } from 'pl-fe/api/hooks/trends/use-suggested-accounts';
import type { EntityStore } from 'pl-fe/entity-store/types';
import type { Account } from 'pl-fe/normalizers/account';
import type { AppDispatch, RootState } from 'pl-fe/store';
@ -35,6 +37,10 @@ const blockDomain = (domain: string) =>
const accounts = selectAccountsByDomain(getState(), domain);
if (!accounts) return;
dispatch(blockDomainSuccess(domain, accounts));
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => !accounts.includes(suggestion.account_id))
: undefined);
}).catch(err => {
dispatch(blockDomainFail(domain, err));
});

View file

@ -1,74 +0,0 @@
import { getClient } from '../api';
import { fetchRelationships } from './accounts';
import { importEntities } from './importer';
import { insertSuggestionsIntoTimeline } from './timelines';
import type { Suggestion } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store';
const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST' as const;
const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS' as const;
const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL' as const;
interface SuggestionsFetchRequestAction {
type: typeof SUGGESTIONS_FETCH_REQUEST;
}
interface SuggestionsFetchSuccessAction {
type: typeof SUGGESTIONS_FETCH_SUCCESS;
suggestions: Array<Suggestion>;
}
interface SuggestionsFetchFailAction {
type: typeof SUGGESTIONS_FETCH_FAIL;
error: any;
skipAlert: true;
}
const fetchSuggestions = (limit = 50) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const client = getClient(state);
const me = state.me;
if (!me) return null;
if (client.features.suggestions) {
dispatch<SuggestionsFetchRequestAction>({ type: SUGGESTIONS_FETCH_REQUEST });
return getClient(getState).myAccount.getSuggestions(limit).then((suggestions) => {
const accounts = suggestions.map(({ account }) => account);
dispatch(importEntities({ accounts }));
dispatch<SuggestionsFetchSuccessAction>({ type: SUGGESTIONS_FETCH_SUCCESS, suggestions });
dispatch(fetchRelationships(accounts.map(({ id }) => id)));
return suggestions;
}).catch(error => {
dispatch<SuggestionsFetchFailAction>({ type: SUGGESTIONS_FETCH_FAIL, error, skipAlert: true });
throw error;
});
} else {
// Do nothing
return null;
}
};
const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch) => {
dispatch(fetchSuggestions(20))?.then(() => dispatch(insertSuggestionsIntoTimeline()));
};
type SuggestionsAction =
| SuggestionsFetchRequestAction
| SuggestionsFetchSuccessAction
| SuggestionsFetchFailAction;
export {
SUGGESTIONS_FETCH_REQUEST,
SUGGESTIONS_FETCH_SUCCESS,
SUGGESTIONS_FETCH_FAIL,
fetchSuggestions,
fetchSuggestionsForTimeline,
type SuggestionsAction,
};

View file

@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { importEntities } from 'pl-fe/actions/importer';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import { useFeatures } from 'pl-fe/hooks/use-features';
import type { Suggestion } from 'pl-api';
type MinifiedSuggestion = Omit<Suggestion, 'account'> & { account_id: string };
const useSuggestedAccounts = () => {
const client = useClient();
const dispatch = useAppDispatch();
const features = useFeatures();
return useQuery({
queryKey: ['suggestions'],
queryFn: () => client.myAccount.getSuggestions().then((suggestions) => {
dispatch(importEntities({ accounts: suggestions.map(({ account }) => account) }));
return suggestions.map(({ account, ...suggestion }): MinifiedSuggestion => ({ account_id: account.id, ...suggestion }));
}),
enabled: features.suggestions || features.suggestionsV2,
});
};
export { useSuggestedAccounts, type MinifiedSuggestion };

View file

@ -3,12 +3,12 @@ import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
import { useSuggestedAccounts } from 'pl-fe/api/hooks/trends/use-suggested-accounts';
import Card, { CardBody, CardTitle } from 'pl-fe/components/ui/card';
import HStack from 'pl-fe/components/ui/hstack';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import Emojify from '../emoji/emojify';
import ActionButton from '../ui/components/action-button';
@ -69,10 +69,9 @@ interface IFeedSuggesetions {
const FeedSuggestions: React.FC<IFeedSuggesetions> = ({ statusId, onMoveUp, onMoveDown }) => {
const intl = useIntl();
const suggestedProfiles = useAppSelector((state) => state.suggestions.items);
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const { data: suggestedProfiles, isLoading } = useSuggestedAccounts();
if (!isLoading && suggestedProfiles.length === 0) return null;
if (!isLoading && suggestedProfiles?.length === 0) return null;
const handleHotkeyMoveUp = (e?: KeyboardEvent): void => {
if (onMoveUp) {
@ -107,7 +106,7 @@ const FeedSuggestions: React.FC<IFeedSuggesetions> = ({ statusId, onMoveUp, onMo
<CardBody>
<HStack space={4} alignItems='center' className='overflow-x-auto md:space-x-0 lg:overflow-x-hidden'>
{suggestedProfiles.slice(0, 4).map((suggestedProfile) => (
{suggestedProfiles?.slice(0, 4).map((suggestedProfile) => (
<SuggestionItem key={suggestedProfile.account_id} accountId={suggestedProfile.account_id} />
))}
</HStack>

View file

@ -5,6 +5,7 @@ import { useSearchParams } from 'react-router-dom-v5-compat';
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/api/hooks/search/use-search';
import { useSuggestedAccounts } from 'pl-fe/api/hooks/trends/use-suggested-accounts';
import { useTrendingLinks } from 'pl-fe/api/hooks/trends/use-trending-links';
import { useTrendingStatuses } from 'pl-fe/api/hooks/trends/use-trending-statuses';
import Hashtag from 'pl-fe/components/hashtag';
@ -19,7 +20,6 @@ import StatusContainer from 'pl-fe/containers/status-container';
import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account';
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useFeatures } from 'pl-fe/hooks/use-features';
import useTrends from 'pl-fe/queries/trends';
@ -65,7 +65,7 @@ const SearchResults = () => {
else setParams(params => ({ ...Object.fromEntries(params.entries()), type: newActiveFilter }));
};
const suggestions = useAppSelector((state) => state.suggestions.items);
const { data: suggestions } = useSuggestedAccounts();
const { data: trendingTags } = useTrends();
const { data: trendingStatuses } = useTrendingStatuses();
const { data: trendingLinks } = useTrendingLinks();

View file

@ -11,7 +11,6 @@ import { fetchMarker } from 'pl-fe/actions/markers';
import { expandNotifications } from 'pl-fe/actions/notifications';
import { register as registerPushNotifications } from 'pl-fe/actions/push-notifications';
import { fetchScheduledStatuses } from 'pl-fe/actions/scheduled-statuses';
import { fetchSuggestionsForTimeline } from 'pl-fe/actions/suggestions';
import { fetchHomeTimeline } from 'pl-fe/actions/timelines';
import { useUserStream } from 'pl-fe/api/hooks/streaming/use-user-stream';
import SidebarNavigation from 'pl-fe/components/sidebar-navigation';
@ -388,9 +387,7 @@ const UI: React.FC<IUI> = ({ children }) => {
dispatch(fetchDraftStatuses());
dispatch(fetchHomeTimeline(false, () => {
dispatch(fetchSuggestionsForTimeline());
}));
dispatch(fetchHomeTimeline());
dispatch(expandNotifications())
// @ts-ignore

View file

@ -35,7 +35,6 @@ import scheduled_statuses from './scheduled-statuses';
import security from './security';
import status_lists from './status-lists';
import statuses from './statuses';
import suggestions from './suggestions';
import tags from './tags';
import timelines from './timelines';
import trending_statuses from './trending-statuses';
@ -74,7 +73,6 @@ const reducers = {
security,
status_lists,
statuses,
suggestions,
tags,
timelines,
trending_statuses,

View file

@ -1,11 +0,0 @@
import reducer from './suggestions';
describe('suggestions reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any).toJS()).toEqual({
items: [],
next: null,
isLoading: false,
});
});
});

View file

@ -1,58 +0,0 @@
import { Record as ImmutableRecord } from 'immutable';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, type AccountsAction } from 'pl-fe/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS, type DomainBlocksAction } from 'pl-fe/actions/domain-blocks';
import {
SUGGESTIONS_FETCH_REQUEST,
SUGGESTIONS_FETCH_SUCCESS,
SUGGESTIONS_FETCH_FAIL,
type SuggestionsAction,
} from 'pl-fe/actions/suggestions';
import type { Suggestion as SuggestionEntity } from 'pl-api';
const ReducerRecord = ImmutableRecord({
items: Array<MinifiedSuggestion>(),
isLoading: false,
});
type State = ReturnType<typeof ReducerRecord>;
const minifySuggestion = ({ account, ...suggestion }: SuggestionEntity) => ({
...suggestion,
account_id: account.id,
});
type MinifiedSuggestion = ReturnType<typeof minifySuggestion>;
const importSuggestions = (state: State, suggestions: SuggestionEntity[]) =>
state.withMutations(state => {
state.update('items', items => items.concat(suggestions.map(minifySuggestion)));
state.set('isLoading', false);
});
const dismissAccount = (state: State, accountId: string) =>
state.update('items', items => items.filter(item => item.account_id !== accountId));
const dismissAccounts = (state: State, accountIds: string[]) =>
state.update('items', items => items.filter(item => !accountIds.includes(item.account_id)));
const suggestionsReducer = (state: State = ReducerRecord(), action: AccountsAction | DomainBlocksAction | SuggestionsAction) => {
switch (action.type) {
case SUGGESTIONS_FETCH_REQUEST:
return state.set('isLoading', true);
case SUGGESTIONS_FETCH_SUCCESS:
return importSuggestions(state, action.suggestions);
case SUGGESTIONS_FETCH_FAIL:
return state.set('isLoading', false);
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS:
return dismissAccount(state, action.relationship.id);
case DOMAIN_BLOCK_SUCCESS:
return dismissAccounts(state, action.accounts);
default:
return state;
}
};
export { suggestionsReducer as default };

View file

@ -203,7 +203,7 @@ const userLists = (state = initialState, action: AccountsAction | DirectoryActio
case PINNED_ACCOUNTS_FETCH_SUCCESS:
return normalizeList(state, ['pinned', action.accountId], action.accounts, action.next);
case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
return normalizeList(state, ['birthday_reminders', action.accountId], action.accounts, action.next);
return normalizeList(state, ['birthday_reminders', action.accountId], action.accounts);
case FAMILIAR_FOLLOWERS_FETCH_SUCCESS:
return normalizeList(state, ['familiar_followers', action.accountId], action.accounts, action.next);
case EVENT_PARTICIPATIONS_FETCH_SUCCESS: