diff --git a/app/javascript/mastodon/components/empty_state/empty_state.module.scss b/app/javascript/mastodon/components/empty_state/empty_state.module.scss
index e9602f2e41..b58c565ac5 100644
--- a/app/javascript/mastodon/components/empty_state/empty_state.module.scss
+++ b/app/javascript/mastodon/components/empty_state/empty_state.module.scss
@@ -35,3 +35,12 @@
text-wrap: pretty;
}
}
+
+[data-color-scheme='dark'] .defaultImage {
+ --color-skin-1: #3a3a50;
+ --color-skin-2: #67678e;
+ --color-skin-3: #44445f;
+ --color-outline: #21212c;
+ --color-shadow: #181820;
+ --color-highlight: #b2b1c8;
+}
diff --git a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx
index 8515a6ea1a..83fce03468 100644
--- a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx
+++ b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx
@@ -29,12 +29,6 @@ export const Default: Story = {
},
};
-export const WithoutMessage: Story = {
- args: {
- message: undefined,
- },
-};
-
export const WithAction: Story = {
args: {
...Default.args,
@@ -42,3 +36,17 @@ export const WithAction: Story = {
children: ,
},
};
+
+export const WithoutImage: Story = {
+ args: {
+ ...Default.args,
+ image: null,
+ },
+};
+
+export const WithoutMessage: Story = {
+ args: {
+ ...Default.args,
+ message: undefined,
+ },
+};
diff --git a/app/javascript/mastodon/components/empty_state/index.tsx b/app/javascript/mastodon/components/empty_state/index.tsx
index 59210bbc87..0ef0d67995 100644
--- a/app/javascript/mastodon/components/empty_state/index.tsx
+++ b/app/javascript/mastodon/components/empty_state/index.tsx
@@ -1,31 +1,39 @@
import { FormattedMessage } from 'react-intl';
+import ElephantImage from '@/images/elephant_ui.svg?react';
+
import classes from './empty_state.module.scss';
+const images = {
+ default: ,
+};
+
/**
* Simple empty state component with a neutral default title and customisable message.
*
- * Action buttons can be passed as `children`
+ * Action buttons can be passed as `children`.
*/
export const EmptyState: React.FC<{
- image?: React.ReactNode;
+ image?: keyof typeof images | React.ReactElement | null;
title?: React.ReactNode;
message?: React.ReactNode;
children?: React.ReactNode;
}> = ({
- image,
+ image = 'default',
title = (
),
message,
children,
}) => {
+ const imageToRender = typeof image === 'string' ? images[image] : image;
+
return (
- {(title || message || image) && (
+ {(title || message || imageToRender) && (
- {image}
+ {imageToRender}
{!!title &&
{title}
}
{!!message &&
{message}
}
diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.module.scss b/app/javascript/mastodon/features/account_featured/components/empty_message.module.scss
deleted file mode 100644
index 25dcf19433..0000000000
--- a/app/javascript/mastodon/features/account_featured/components/empty_message.module.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-[data-color-scheme='dark'] .image {
- --color-skin-1: #3a3a50;
- --color-skin-2: #67678e;
- --color-skin-3: #44445f;
- --color-outline: #21212c;
- --color-shadow: #181820;
- --color-highlight: #b2b1c8;
-}
diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
index 35b67cd641..e1de024a69 100644
--- a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
+++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
@@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
-import ElephantImage from '@/images/elephant_ui.svg?react';
import { openModal } from '@/mastodon/actions/modal';
import { Button } from '@/mastodon/components/button';
import { EmptyState } from '@/mastodon/components/empty_state';
@@ -14,8 +13,6 @@ import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { useAppDispatch } from '@/mastodon/store';
-import classes from './empty_message.module.scss';
-
interface EmptyMessageProps {
suspended: boolean;
hidden: boolean;
@@ -54,14 +51,11 @@ export const EmptyMessage: React.FC
= ({
const hasCollections = areCollectionsEnabled();
- const image = ;
-
if (me === accountId) {
if (hasCollections) {
// Return only here to insert the "Create a collection" button as the action for the empty state.
return (
= ({
}
}
- return ;
+ return ;
};
diff --git a/app/javascript/mastodon/features/collections/editor/accounts.tsx b/app/javascript/mastodon/features/collections/editor/accounts.tsx
index 654f6265e6..0abca399d5 100644
--- a/app/javascript/mastodon/features/collections/editor/accounts.tsx
+++ b/app/javascript/mastodon/features/collections/editor/accounts.tsx
@@ -4,22 +4,16 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
-import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
-import CheckIcon from '@/material-icons/400-24px/check.svg?react';
-import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { showAlertForError } from 'mastodon/actions/alerts';
import { openModal } from 'mastodon/actions/modal';
import { apiFollowAccount } from 'mastodon/api/accounts';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Account } from 'mastodon/components/account';
import { Avatar } from 'mastodon/components/avatar';
-import { Badge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { EmptyState } from 'mastodon/components/empty_state';
-import { FormStack, Combobox } from 'mastodon/components/form_fields';
-import { Icon } from 'mastodon/components/icon';
-import { IconButton } from 'mastodon/components/icon_button';
+import { FormStack, ComboboxField } from 'mastodon/components/form_fields';
import {
Article,
ItemList,
@@ -37,75 +31,35 @@ import {
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
import classes from './styles.module.scss';
-import { WizardStepHeader } from './wizard_step_header';
+import { WizardStepTitle } from './wizard_step_title';
-const MAX_ACCOUNT_COUNT = 25;
-
-function isOlderThanAWeek(date?: string): boolean {
- if (!date) return false;
-
- const targetDate = new Date(date);
- const sevenDaysAgo = new Date();
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
- return targetDate < sevenDaysAgo;
-}
+const MAX_ACCOUNT_COUNT = 3;
const AddedAccountItem: React.FC<{
accountId: string;
onRemove: (id: string) => void;
}> = ({ accountId, onRemove }) => {
- const intl = useIntl();
- const account = useAccount(accountId);
-
const handleRemoveAccount = useCallback(() => {
onRemove(accountId);
}, [accountId, onRemove]);
- const lastStatusAt = account?.last_status_at;
-
- const lastPostHint = useMemo(
- () =>
- (!lastStatusAt || isOlderThanAWeek(lastStatusAt)) && (
-
- }
- icon={}
- className={classes.accountBadge}
- />
- ),
- [lastStatusAt],
- );
-
return (
-
-
+
+
);
};
interface SuggestionItem {
id: string;
- isSelected: boolean;
}
-const SuggestedAccountItem: React.FC = ({ id, isSelected }) => {
+const SuggestedAccountItem: React.FC = ({ id }) => {
const account = useAccount(id);
if (!account) return null;
@@ -114,23 +68,15 @@ const SuggestedAccountItem: React.FC = ({ id, isSelected }) => {
<>
- {isSelected && (
-
- )}
>
);
};
const renderAccountItem = (item: SuggestionItem) => (
-
+
);
const getItemId = (item: SuggestionItem) => item.id;
-const getIsItemSelected = (item: SuggestionItem) => item.isSelected;
export const CollectionAccounts: React.FC<{
collection?: ApiCollectionJSON | null;
@@ -139,9 +85,8 @@ export const CollectionAccounts: React.FC<{
const dispatch = useAppDispatch();
const history = useHistory();
- const { id, items } = collection ?? {};
+ const { id, items: collectionItems } = collection ?? {};
const isEditMode = !!id;
- const collectionItems = items;
const addedAccountIds = useAppSelector(
(state) => state.collections.editor.accountIds,
@@ -157,22 +102,25 @@ export const CollectionAccounts: React.FC<{
const [searchValue, setSearchValue] = useState('');
+ const hasAccounts = accountIds.length > 0;
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
const {
accountIds: suggestedAccountIds,
isLoading: isLoadingSuggestions,
searchAccounts,
+ resetAccounts,
} = useSearchAccounts({
withRelationships: true,
filterResults: (account) =>
+ !accountIds.includes(account.id) &&
// Only suggest accounts who allow being featured/recommended
account.feature_approval.current_user === 'automatic',
});
const suggestedItems = suggestedAccountIds.map((id) => ({
id,
- isSelected: accountIds.includes(id),
+ isDisabled: accountIds.includes(id),
}));
const handleSearchValueChange = useCallback(
@@ -242,12 +190,12 @@ export const CollectionAccounts: React.FC<{
);
const addAccountItem = useCallback(
- (accountId: string) => {
- confirmFollowStatus(accountId, () => {
+ (item: SuggestionItem) => {
+ confirmFollowStatus(item.id, () => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
- value: [...accountIds, accountId],
+ value: [...accountIds, item.id],
}),
);
});
@@ -255,17 +203,6 @@ export const CollectionAccounts: React.FC<{
[accountIds, confirmFollowStatus, dispatch],
);
- const toggleAccountItem = useCallback(
- (item: SuggestionItem) => {
- if (accountIds.includes(item.id)) {
- removeAccountItem(item.id);
- } else {
- addAccountItem(item.id);
- }
- },
- [accountIds, addAccountItem, removeAccountItem],
- );
-
const instantRemoveAccountItem = useCallback(
(accountId: string) => {
const itemId = collectionItems?.find(
@@ -289,23 +226,16 @@ export const CollectionAccounts: React.FC<{
);
const instantAddAccountItem = useCallback(
- (collectionId: string, accountId: string) => {
- confirmFollowStatus(accountId, () => {
- void dispatch(addCollectionItem({ collectionId, accountId }));
+ (item: SuggestionItem) => {
+ confirmFollowStatus(item.id, () => {
+ if (id) {
+ void dispatch(
+ addCollectionItem({ collectionId: id, accountId: item.id }),
+ );
+ }
});
},
- [confirmFollowStatus, dispatch],
- );
-
- const instantToggleAccountItem = useCallback(
- (item: SuggestionItem) => {
- if (accountIds.includes(item.id)) {
- instantRemoveAccountItem(item.id);
- } else if (id) {
- instantAddAccountItem(id, item.id);
- }
- },
- [accountIds, id, instantAddAccountItem, instantRemoveAccountItem],
+ [confirmFollowStatus, dispatch, id],
);
const handleRemoveAccountItem = useCallback(
@@ -319,6 +249,20 @@ export const CollectionAccounts: React.FC<{
[isEditMode, instantRemoveAccountItem, removeAccountItem],
);
+ const handleSelectItem = useCallback(
+ (item: SuggestionItem) => {
+ if (isEditMode) {
+ instantAddAccountItem(item);
+ } else {
+ addAccountItem(item);
+ }
+
+ setSearchValue('');
+ resetAccounts();
+ },
+ [addAccountItem, instantAddAccountItem, isEditMode, resetAccounts],
+ );
+
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
@@ -333,117 +277,114 @@ export const CollectionAccounts: React.FC<{
);
const inputId = useId();
- const inputLabel = intl.formatMessage({
- id: 'collections.search_accounts_label',
- defaultMessage: 'Search for accounts to add…',
- });
+ const AccountsHeadingElement = id ? 'h2' : 'h3';
return (