Implement collection limit on frontend (#38786)

This commit is contained in:
diondiondion 2026-04-23 14:44:30 +02:00 committed by GitHub
parent d7b60a2cb6
commit 2f0db28aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 79 additions and 26 deletions

View File

@ -25,12 +25,9 @@ import Column from 'mastodon/features/ui/components/column';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import {
fetchAccountCollections,
selectAccountCollections,
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useAccountCollections } from '../collections';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { areCollectionsEnabled } from '../collections/utils';
@ -59,10 +56,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
useEffect(() => {
if (accountId) {
void dispatch(fetchEndorsedAccounts({ accountId }));
if (collectionsEnabled) {
void dispatch(fetchAccountCollections({ accountId }));
}
}
}, [accountId, dispatch]);
@ -73,9 +66,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
ImmutableList(),
) as ImmutableList<string>,
);
const { collections, status: collectionsLoadStatus } = useAppSelector(
(state) => selectAccountCollections(state, accountId ?? null),
);
const { collections, status: collectionsLoadStatus } =
useAccountCollections(accountId);
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
collections,

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import {
@ -12,6 +12,9 @@ import {
useLocation,
} from 'react-router-dom';
import { Callout } from '@/mastodon/components/callout';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { initialState } from '@/mastodon/initial_state';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
@ -22,8 +25,11 @@ import {
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useAccountCollections } from '..';
import { CollectionAccounts } from './accounts';
import { CollectionDetails } from './details';
import classes from './styles.module.scss';
export const messages = defineMessages({
create: {
@ -61,11 +67,14 @@ function usePageTitle(id: string | null) {
}
}
export const userCollectionLimit = initialState?.role?.collection_limit ?? 0;
export const CollectionEditorPage: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const accountId = useCurrentAccountId();
const { id = null } = useParams<{ id?: string }>();
const { path } = useRouteMatch();
const collection = useAppSelector((state) =>
@ -73,7 +82,18 @@ export const CollectionEditorPage: React.FC<{
);
const editorStateId = useAppSelector((state) => state.collections.editor.id);
const isEditMode = !!id;
const isLoading = isEditMode && !collection;
// When creating a new collection, we load the current account's collections
// to determine if they're allowed to create more.
const { collections: collectionList, status: collectionListStatus } =
useAccountCollections(isEditMode ? null : accountId);
const isLoading =
(isEditMode && !collection) ||
(!isEditMode && collectionListStatus === 'loading');
const canCreateMoreCollections =
isEditMode || collectionList.length < userCollectionLimit;
useEffect(() => {
if (id) {
@ -108,7 +128,7 @@ export const CollectionEditorPage: React.FC<{
<div className='scrollable'>
{isLoading ? (
<LoadingIndicator />
) : (
) : canCreateMoreCollections ? (
<Switch>
<Route
exact
@ -123,6 +143,8 @@ export const CollectionEditorPage: React.FC<{
render={() => <CollectionDetails />}
/>
</Switch>
) : (
<MaxCollectionsCallout />
)}
</div>
@ -133,3 +155,21 @@ export const CollectionEditorPage: React.FC<{
</Column>
);
};
export const MaxCollectionsCallout: React.FC = () => (
<Callout
className={classes.maxCollectionsError}
title={
<FormattedMessage
id='collections.maximum_collection_count_reached'
defaultMessage='You have created the maximum number of collections'
/>
}
>
<FormattedMessage
id='collections.maximum_collection_count_description'
defaultMessage='Your server allows creation of up to {count} collections.'
values={{ count: userCollectionLimit }}
/>
</Callout>
);

View File

@ -77,3 +77,7 @@
.suggestionGroup {
padding-bottom: 4px;
}
.maxCollectionsError {
margin: 16px;
}

View File

@ -25,7 +25,12 @@ import {
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { CollectionListItem } from './components/collection_list_item';
import { messages as editorMessages } from './editor';
import {
messages as editorMessages,
MaxCollectionsCallout,
userCollectionLimit,
} from './editor';
import { areCollectionsEnabled } from './utils';
const messages = defineMessages({
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
@ -35,24 +40,27 @@ const messages = defineMessages({
},
});
export function useAccountCollections(accountId: string | null | undefined) {
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && areCollectionsEnabled()) {
void dispatch(fetchAccountCollections({ accountId }));
}
}, [dispatch, accountId]);
return useAppSelector((state) => selectAccountCollections(state, accountId));
}
export const Collections: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const me = useCurrentAccountId();
const accountId = useAccountId();
const account = useAccount(accountId);
const { collections, status } = useAppSelector((state) =>
selectAccountCollections(state, accountId),
);
useEffect(() => {
if (accountId) {
void dispatch(fetchAccountCollections({ accountId }));
}
}, [dispatch, accountId]);
const { collections, status } = useAccountCollections(accountId);
const emptyMessage =
status === 'error' || !accountId ? (
@ -79,6 +87,7 @@ export const Collections: React.FC<{
</>
);
const canCreateMoreCollections = collections.length < userCollectionLimit;
const isOwnCollection = accountId === me;
const titleMessage = isOwnCollection
? messages.headingMe
@ -99,7 +108,9 @@ export const Collections: React.FC<{
iconComponent={CollectionsFilledIcon}
multiColumn={multiColumn}
extraButton={
isOwnCollection && (
isOwnCollection &&
status === 'idle' &&
canCreateMoreCollections && (
<Link
to='/collections/new'
className='column-header__button'
@ -113,6 +124,9 @@ export const Collections: React.FC<{
/>
<Scrollable>
{status === 'idle' && !canCreateMoreCollections && (
<MaxCollectionsCallout />
)}
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
{collections.map((item, index) => (
<CollectionListItem

View File

@ -57,6 +57,7 @@ interface Role {
permissions: string;
color: string;
highlighted: boolean;
collection_limit: number;
}
interface InitialWrapstodonState {

View File

@ -398,6 +398,8 @@
"collections.manage_accounts": "Manage accounts",
"collections.mark_as_sensitive": "Mark as sensitive",
"collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The collection name will still be visible.",
"collections.maximum_collection_count_description": "Your server allows creation of up to {count} collections.",
"collections.maximum_collection_count_reached": "You have created the maximum number of collections",
"collections.name_length_hint": "40 characters limit",
"collections.new_collection": "New collection",
"collections.no_collections_yet": "No collections yet.",