From 0caf334891e0a5f4e995f1d667eb83d3aad7e41e Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 29 May 2026 17:27:57 +0200 Subject: [PATCH] Reduce account overfetching when displaying collection lists (#39220) --- .../mastodon/actions/accounts_typed.ts | 10 +++++++ app/javascript/mastodon/api/accounts.ts | 5 ++++ app/javascript/mastodon/api/lists.ts | 2 +- .../mastodon/features/lists/members.tsx | 4 +-- .../mastodon/features/lists/new.tsx | 4 +-- .../mastodon/reducers/slices/collections.ts | 28 +++++++++++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts index fe7c7327ce..3d8396c81a 100644 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -3,6 +3,7 @@ import { createAction } from '@reduxjs/toolkit'; import { apiRemoveAccountFromFollowers, apiGetEndorsedAccounts, + apiGetAccounts, } from 'mastodon/api/accounts'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; @@ -113,3 +114,12 @@ export const fetchEndorsedAccounts = createDataLoadingThunk( return data; }, ); + +export const fetchAccounts = createDataLoadingThunk( + 'accounts/multi_accounts', + ({ accountIds }: { accountIds: string[] }) => apiGetAccounts(accountIds), + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data)); + return data; + }, +); diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 2229d17c56..52c5b017d9 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -19,6 +19,11 @@ import type { ApiProfileUpdateParams, } from '../api_types/profile'; +export const apiGetAccounts = (ids: string[]) => + apiRequestGet('v1/accounts', { + id: ids, + }); + export const apiSubmitAccountNote = (id: string, value: string) => apiRequestPost(`v1/accounts/${id}/note`, { comment: value, diff --git a/app/javascript/mastodon/api/lists.ts b/app/javascript/mastodon/api/lists.ts index fa7e6e4554..a3f4b5be95 100644 --- a/app/javascript/mastodon/api/lists.ts +++ b/app/javascript/mastodon/api/lists.ts @@ -15,7 +15,7 @@ export const apiUpdate = (list: Partial) => export const apiGetLists = () => apiRequestGet('v1/lists'); -export const apiGetAccounts = (listId: string) => +export const apiGetListAccounts = (listId: string) => apiRequestGet(`v1/lists/${listId}/accounts`, { limit: 0, }); diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index 66ea1bb127..a788034030 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -15,7 +15,7 @@ import { fetchList } from 'mastodon/actions/lists'; import { openModal } from 'mastodon/actions/modal'; import { apiFollowAccount } from 'mastodon/api/accounts'; import { - apiGetAccounts, + apiGetListAccounts, apiAddAccountToList, apiRemoveAccountFromList, } from 'mastodon/api/lists'; @@ -184,7 +184,7 @@ const ListMembers: React.FC<{ if (id) { dispatch(fetchList(id)); - void apiGetAccounts(id) + void apiGetListAccounts(id) .then((data) => { dispatch(importFetchedAccounts(data)); setAccountIds(data.map((a) => a.id)); diff --git a/app/javascript/mastodon/features/lists/new.tsx b/app/javascript/mastodon/features/lists/new.tsx index b2f916b738..e8f6343278 100644 --- a/app/javascript/mastodon/features/lists/new.tsx +++ b/app/javascript/mastodon/features/lists/new.tsx @@ -12,7 +12,7 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react' import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import { fetchList } from 'mastodon/actions/lists'; import { createList, updateList } from 'mastodon/actions/lists_typed'; -import { apiGetAccounts } from 'mastodon/api/lists'; +import { apiGetListAccounts } from 'mastodon/api/lists'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { RepliesPolicyType } from 'mastodon/api_types/lists'; import { Avatar } from 'mastodon/components/avatar'; @@ -44,7 +44,7 @@ const MembersLink: React.FC<{ const [avatarAccounts, setAvatarAccounts] = useState([]); useEffect(() => { - void apiGetAccounts(id) + void apiGetListAccounts(id) .then((data) => { setAvatarCount(data.length); setAvatarAccounts(data.slice(0, 3)); diff --git a/app/javascript/mastodon/reducers/slices/collections.ts b/app/javascript/mastodon/reducers/slices/collections.ts index aa45bfc6e9..ec2143351a 100644 --- a/app/javascript/mastodon/reducers/slices/collections.ts +++ b/app/javascript/mastodon/reducers/slices/collections.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { fetchAccounts } from '@/mastodon/actions/accounts_typed'; import { importFetchedAccounts } from '@/mastodon/actions/importer'; import { apiCreateCollection, @@ -20,6 +21,7 @@ import type { CollectionAccountItem, } from '@/mastodon/api_types/collections'; import { initialState, me } from '@/mastodon/initial_state'; +import type { AppDispatch } from '@/mastodon/store'; import { createAppAsyncThunk, createAppSelector, @@ -322,16 +324,42 @@ const collectionSlice = createSlice({ }, }); +/** + * Prefetch accounts whose avatars will be displayed in the collection list + */ +async function importAccountsForPreviewCard( + collections: ApiCollectionJSON[], + dispatch: AppDispatch, +) { + const previewAccountIds = collections + .flatMap((collection) => + collection.items.slice(0, 3).map((item) => item.account_id), + ) + .filter((id): id is string => !!id); + + await dispatch( + fetchAccounts({ + accountIds: previewAccountIds, + }), + ); +} + export const fetchCollectionsCreatedByAccount = createDataLoadingThunk( `${collectionSlice.name}/fetchCollectionsCreatedByAccount`, ({ accountId }: { accountId: string }) => apiGetCollectionsCreatedByAccount(accountId), + async ({ collections }, { dispatch }) => { + await importAccountsForPreviewCard(collections, dispatch); + }, ); export const fetchCollectionsFeaturingAccount = createDataLoadingThunk( `${collectionSlice.name}/fetchCollectionsFeaturingAccount`, ({ accountId }: { accountId: string }) => apiGetCollectionsFeaturingAccount(accountId), + async ({ collections }, { dispatch }) => { + await importAccountsForPreviewCard(collections, dispatch); + }, ); export const fetchCollection = createDataLoadingThunk(