Implement collection limit on frontend (#38786)
This commit is contained in:
parent
d7b60a2cb6
commit
2f0db28aa4
@ -25,12 +25,9 @@ import Column from 'mastodon/features/ui/components/column';
|
|||||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
||||||
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
||||||
import {
|
|
||||||
fetchAccountCollections,
|
|
||||||
selectAccountCollections,
|
|
||||||
} from 'mastodon/reducers/slices/collections';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { useAccountCollections } from '../collections';
|
||||||
import { CollectionListItem } from '../collections/components/collection_list_item';
|
import { CollectionListItem } from '../collections/components/collection_list_item';
|
||||||
import { areCollectionsEnabled } from '../collections/utils';
|
import { areCollectionsEnabled } from '../collections/utils';
|
||||||
|
|
||||||
@ -59,10 +56,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
void dispatch(fetchEndorsedAccounts({ accountId }));
|
void dispatch(fetchEndorsedAccounts({ accountId }));
|
||||||
|
|
||||||
if (collectionsEnabled) {
|
|
||||||
void dispatch(fetchAccountCollections({ accountId }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [accountId, dispatch]);
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
@ -73,9 +66,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
ImmutableList(),
|
ImmutableList(),
|
||||||
) as ImmutableList<string>,
|
) as ImmutableList<string>,
|
||||||
);
|
);
|
||||||
const { collections, status: collectionsLoadStatus } = useAppSelector(
|
const { collections, status: collectionsLoadStatus } =
|
||||||
(state) => selectAccountCollections(state, accountId ?? null),
|
useAccountCollections(accountId);
|
||||||
);
|
|
||||||
|
|
||||||
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
||||||
collections,
|
collections,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import {
|
import {
|
||||||
@ -12,6 +12,9 @@ import {
|
|||||||
useLocation,
|
useLocation,
|
||||||
} from 'react-router-dom';
|
} 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 ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { Column } from 'mastodon/components/column';
|
import { Column } from 'mastodon/components/column';
|
||||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
@ -22,8 +25,11 @@ import {
|
|||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { useAccountCollections } from '..';
|
||||||
|
|
||||||
import { CollectionAccounts } from './accounts';
|
import { CollectionAccounts } from './accounts';
|
||||||
import { CollectionDetails } from './details';
|
import { CollectionDetails } from './details';
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
create: {
|
create: {
|
||||||
@ -61,11 +67,14 @@ function usePageTitle(id: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userCollectionLimit = initialState?.role?.collection_limit ?? 0;
|
||||||
|
|
||||||
export const CollectionEditorPage: React.FC<{
|
export const CollectionEditorPage: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const accountId = useCurrentAccountId();
|
||||||
const { id = null } = useParams<{ id?: string }>();
|
const { id = null } = useParams<{ id?: string }>();
|
||||||
const { path } = useRouteMatch();
|
const { path } = useRouteMatch();
|
||||||
const collection = useAppSelector((state) =>
|
const collection = useAppSelector((state) =>
|
||||||
@ -73,7 +82,18 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
);
|
);
|
||||||
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
||||||
const isEditMode = !!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(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -108,7 +128,7 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
<div className='scrollable'>
|
<div className='scrollable'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
) : (
|
) : canCreateMoreCollections ? (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
@ -123,6 +143,8 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
render={() => <CollectionDetails />}
|
render={() => <CollectionDetails />}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
) : (
|
||||||
|
<MaxCollectionsCallout />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -133,3 +155,21 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
</Column>
|
</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>
|
||||||
|
);
|
||||||
|
|||||||
@ -77,3 +77,7 @@
|
|||||||
.suggestionGroup {
|
.suggestionGroup {
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maxCollectionsError {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,12 @@ import {
|
|||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { CollectionListItem } from './components/collection_list_item';
|
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({
|
const messages = defineMessages({
|
||||||
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
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<{
|
export const Collections: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const me = useCurrentAccountId();
|
const me = useCurrentAccountId();
|
||||||
const accountId = useAccountId();
|
const accountId = useAccountId();
|
||||||
const account = useAccount(accountId);
|
const account = useAccount(accountId);
|
||||||
|
|
||||||
const { collections, status } = useAppSelector((state) =>
|
const { collections, status } = useAccountCollections(accountId);
|
||||||
selectAccountCollections(state, accountId),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (accountId) {
|
|
||||||
void dispatch(fetchAccountCollections({ accountId }));
|
|
||||||
}
|
|
||||||
}, [dispatch, accountId]);
|
|
||||||
|
|
||||||
const emptyMessage =
|
const emptyMessage =
|
||||||
status === 'error' || !accountId ? (
|
status === 'error' || !accountId ? (
|
||||||
@ -79,6 +87,7 @@ export const Collections: React.FC<{
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canCreateMoreCollections = collections.length < userCollectionLimit;
|
||||||
const isOwnCollection = accountId === me;
|
const isOwnCollection = accountId === me;
|
||||||
const titleMessage = isOwnCollection
|
const titleMessage = isOwnCollection
|
||||||
? messages.headingMe
|
? messages.headingMe
|
||||||
@ -99,7 +108,9 @@ export const Collections: React.FC<{
|
|||||||
iconComponent={CollectionsFilledIcon}
|
iconComponent={CollectionsFilledIcon}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={
|
extraButton={
|
||||||
isOwnCollection && (
|
isOwnCollection &&
|
||||||
|
status === 'idle' &&
|
||||||
|
canCreateMoreCollections && (
|
||||||
<Link
|
<Link
|
||||||
to='/collections/new'
|
to='/collections/new'
|
||||||
className='column-header__button'
|
className='column-header__button'
|
||||||
@ -113,6 +124,9 @@ export const Collections: React.FC<{
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
|
{status === 'idle' && !canCreateMoreCollections && (
|
||||||
|
<MaxCollectionsCallout />
|
||||||
|
)}
|
||||||
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
||||||
{collections.map((item, index) => (
|
{collections.map((item, index) => (
|
||||||
<CollectionListItem
|
<CollectionListItem
|
||||||
|
|||||||
@ -57,6 +57,7 @@ interface Role {
|
|||||||
permissions: string;
|
permissions: string;
|
||||||
color: string;
|
color: string;
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
|
collection_limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitialWrapstodonState {
|
interface InitialWrapstodonState {
|
||||||
|
|||||||
@ -398,6 +398,8 @@
|
|||||||
"collections.manage_accounts": "Manage accounts",
|
"collections.manage_accounts": "Manage accounts",
|
||||||
"collections.mark_as_sensitive": "Mark as sensitive",
|
"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.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.name_length_hint": "40 characters limit",
|
||||||
"collections.new_collection": "New collection",
|
"collections.new_collection": "New collection",
|
||||||
"collections.no_collections_yet": "No collections yet.",
|
"collections.no_collections_yet": "No collections yet.",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user