= ({
}, [setError]);
const avatar = (
-
= ({
)}
{counter && (
-
{counter}
-
+
)}
-
+
);
if (withLink) {
diff --git a/app/javascript/mastodon/features/collections/styles.module.scss b/app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss
similarity index 80%
rename from app/javascript/mastodon/features/collections/styles.module.scss
rename to app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss
index ebcab70d59..74eac9f3e1 100644
--- a/app/javascript/mastodon/features/collections/styles.module.scss
+++ b/app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss
@@ -1,4 +1,4 @@
-.collectionItemWrapper {
+.wrapper {
display: flex;
align-items: center;
gap: 16px;
@@ -7,13 +7,13 @@
border-bottom: 1px solid var(--color-border-primary);
}
-.collectionItemContent {
+.content {
position: relative;
flex-grow: 1;
padding: 15px 5px;
}
-.collectionItemLink {
+.link {
display: block;
margin-bottom: 2px;
font-size: 15px;
@@ -33,16 +33,19 @@
}
}
-.collectionItemInfo {
+.info {
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+.metaList {
--gap: 0.75ch;
display: flex;
gap: var(--gap);
- font-size: 13px;
- color: var(--color-text-secondary);
- & > li:not(:first-child)::before {
+ & > li:not(:last-child)::after {
content: '·';
- margin-inline-end: var(--gap);
+ margin-inline-start: var(--gap);
}
}
diff --git a/app/javascript/mastodon/features/collections/detail/collection_list_item.tsx b/app/javascript/mastodon/features/collections/detail/collection_list_item.tsx
new file mode 100644
index 0000000000..51a7e67254
--- /dev/null
+++ b/app/javascript/mastodon/features/collections/detail/collection_list_item.tsx
@@ -0,0 +1,67 @@
+import { useId } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
+import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
+
+import classes from './collection_list_item.module.scss';
+import { CollectionMenu } from './collection_menu';
+
+export const CollectionMetaData: React.FC<{
+ collection: ApiCollectionJSON;
+ className?: string;
+}> = ({ collection, className }) => {
+ return (
+
+
+
+ ),
+ }}
+ tagName='li'
+ />
+
+ );
+};
+
+export const CollectionListItem: React.FC<{
+ collection: ApiCollectionJSON;
+}> = ({ collection }) => {
+ const { id, name } = collection;
+ const linkId = useId();
+
+ return (
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/javascript/mastodon/features/collections/detail/collection_menu.tsx b/app/javascript/mastodon/features/collections/detail/collection_menu.tsx
new file mode 100644
index 0000000000..cf65be5496
--- /dev/null
+++ b/app/javascript/mastodon/features/collections/detail/collection_menu.tsx
@@ -0,0 +1,91 @@
+import { useCallback, useMemo } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
+import { openModal } from 'mastodon/actions/modal';
+import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
+import { Dropdown } from 'mastodon/components/dropdown_menu';
+import { IconButton } from 'mastodon/components/icon_button';
+import { useAppDispatch } from 'mastodon/store';
+
+import { messages as editorMessages } from '../editor';
+
+const messages = defineMessages({
+ view: {
+ id: 'collections.view_collection',
+ defaultMessage: 'View collection',
+ },
+ delete: {
+ id: 'collections.delete_collection',
+ defaultMessage: 'Delete collection',
+ },
+ more: { id: 'status.more', defaultMessage: 'More' },
+});
+
+export const CollectionMenu: React.FC<{
+ collection: ApiCollectionJSON;
+ context: 'list' | 'collection';
+ className?: string;
+}> = ({ collection, context, className }) => {
+ const dispatch = useAppDispatch();
+ const intl = useIntl();
+
+ const { id, name } = collection;
+
+ const handleDeleteClick = useCallback(() => {
+ dispatch(
+ openModal({
+ modalType: 'CONFIRM_DELETE_COLLECTION',
+ modalProps: {
+ name,
+ id,
+ },
+ }),
+ );
+ }, [dispatch, id, name]);
+
+ const menu = useMemo(() => {
+ const commonItems = [
+ {
+ text: intl.formatMessage(editorMessages.manageAccounts),
+ to: `/collections/${id}/edit`,
+ },
+ {
+ text: intl.formatMessage(editorMessages.editDetails),
+ to: `/collections/${id}/edit/details`,
+ },
+ {
+ text: intl.formatMessage(editorMessages.editSettings),
+ to: `/collections/${id}/edit/settings`,
+ },
+ null,
+ {
+ text: intl.formatMessage(messages.delete),
+ action: handleDeleteClick,
+ dangerous: true,
+ },
+ ];
+
+ if (context === 'list') {
+ return [
+ { text: intl.formatMessage(messages.view), to: `/collections/${id}` },
+ null,
+ ...commonItems,
+ ];
+ } else {
+ return commonItems;
+ }
+ }, [intl, id, handleDeleteClick, context]);
+
+ return (
+
+
+
+ );
+};
diff --git a/app/javascript/mastodon/features/collections/detail/index.tsx b/app/javascript/mastodon/features/collections/detail/index.tsx
new file mode 100644
index 0000000000..7b04800aab
--- /dev/null
+++ b/app/javascript/mastodon/features/collections/detail/index.tsx
@@ -0,0 +1,178 @@
+import { useCallback, useEffect } from 'react';
+
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router';
+
+import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
+import ShareIcon from '@/material-icons/400-24px/share.svg?react';
+import { showAlert } from 'mastodon/actions/alerts';
+import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
+import { Account } from 'mastodon/components/account';
+import { Avatar } from 'mastodon/components/avatar';
+import { Column } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import { LinkedDisplayName } from 'mastodon/components/display_name';
+import { IconButton } from 'mastodon/components/icon_button';
+import ScrollableList from 'mastodon/components/scrollable_list';
+import { Tag } from 'mastodon/components/tags/tag';
+import { useAccount } from 'mastodon/hooks/useAccount';
+import { me } from 'mastodon/initial_state';
+import { fetchCollection } from 'mastodon/reducers/slices/collections';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+import { CollectionMetaData } from './collection_list_item';
+import { CollectionMenu } from './collection_menu';
+import classes from './styles.module.scss';
+
+const messages = defineMessages({
+ empty: {
+ id: 'collections.accounts.empty_title',
+ defaultMessage: 'This collection is empty',
+ },
+ loading: {
+ id: 'collections.detail.loading',
+ defaultMessage: 'Loading collection…',
+ },
+ share: {
+ id: 'collections.detail.share',
+ defaultMessage: 'Share this collection',
+ },
+ accounts: {
+ id: 'collections.detail.accounts_heading',
+ defaultMessage: 'Accounts',
+ },
+});
+
+const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
+ const account = useAccount(id);
+ const author = (
+
+
+
+
+ );
+
+ if (id === me) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+};
+
+const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
+ collection,
+}) => {
+ const intl = useIntl();
+ const { name, description, tag } = collection;
+ const dispatch = useAppDispatch();
+
+ const handleShare = useCallback(() => {
+ dispatch(showAlert({ message: 'Collection sharing not yet implemented' }));
+ }, [dispatch]);
+
+ return (
+
+
+
+ {tag && (
+ // TODO: Make non-interactive tag component
+
+ )}
+
{name}
+
+
+
+
+
+
+ {description &&
{description}
}
+
+
+
{intl.formatMessage(messages.accounts)}
+
+ );
+};
+
+export const CollectionDetailPage: React.FC<{
+ multiColumn?: boolean;
+}> = ({ multiColumn }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const { id } = useParams<{ id?: string }>();
+ const collection = useAppSelector((state) =>
+ id ? state.collections.collections[id] : undefined,
+ );
+
+ const isLoading = !!id && !collection;
+
+ useEffect(() => {
+ if (id) {
+ void dispatch(fetchCollection({ collectionId: id }));
+ }
+ }, [dispatch, id]);
+
+ const pageTitle = collection?.name ?? intl.formatMessage(messages.loading);
+
+ return (
+
+
+
+ : null
+ }
+ >
+ {collection?.items.map(({ account_id }) =>
+ account_id ? (
+
+ ) : null,
+ )}
+
+
+
+ {pageTitle}
+
+
+
+ );
+};
diff --git a/app/javascript/mastodon/features/collections/detail/styles.module.scss b/app/javascript/mastodon/features/collections/detail/styles.module.scss
new file mode 100644
index 0000000000..cb94f2894c
--- /dev/null
+++ b/app/javascript/mastodon/features/collections/detail/styles.module.scss
@@ -0,0 +1,74 @@
+.header {
+ padding: 16px;
+ border-bottom: 1px solid var(--color-border-primary);
+}
+
+.titleWithMenu {
+ display: flex;
+ align-items: start;
+ gap: 10px;
+}
+
+.titleWrapper {
+ flex-grow: 1;
+ min-width: 0;
+}
+
+.tag {
+ margin-bottom: 4px;
+ margin-inline-start: -8px;
+}
+
+.name {
+ font-size: 28px;
+ line-height: 1.2;
+ overflow-wrap: anywhere;
+}
+
+.description {
+ font-size: 15px;
+ margin-top: 8px;
+}
+
+.headerButtonWrapper {
+ display: flex;
+ gap: 8px;
+}
+
+.iconButton {
+ box-sizing: content-box;
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid var(--color-border-primary);
+}
+
+.authorNote {
+ margin-top: 8px;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+.metaData {
+ margin-top: 16px;
+ font-size: 15px;
+}
+
+.displayNameWithAvatar {
+ display: inline-flex;
+ gap: 4px;
+ align-items: baseline;
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+ }
+
+ > :global(.account__avatar) {
+ align-self: center;
+ }
+}
diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx
index f931a42c6c..9d5a94b8aa 100644
--- a/app/javascript/mastodon/features/collections/editor/details.tsx
+++ b/app/javascript/mastodon/features/collections/editor/details.tsx
@@ -68,7 +68,7 @@ export const CollectionDetails: React.FC<{
};
void dispatch(updateCollection({ payload })).then(() => {
- history.push(`/collections`);
+ history.push(`/collections/${id}`);
});
} else {
const payload: Partial
= {
diff --git a/app/javascript/mastodon/features/collections/editor/settings.tsx b/app/javascript/mastodon/features/collections/editor/settings.tsx
index 10fb295f83..184b51187c 100644
--- a/app/javascript/mastodon/features/collections/editor/settings.tsx
+++ b/app/javascript/mastodon/features/collections/editor/settings.tsx
@@ -68,7 +68,7 @@ export const CollectionSettings: React.FC<{
};
void dispatch(updateCollection({ payload })).then(() => {
- history.push(`/collections`);
+ history.push(`/collections/${id}`);
});
} else {
const payload: ApiCreateCollectionPayload = {
diff --git a/app/javascript/mastodon/features/collections/index.tsx b/app/javascript/mastodon/features/collections/index.tsx
index dd26174f38..607d7fe4f3 100644
--- a/app/javascript/mastodon/features/collections/index.tsx
+++ b/app/javascript/mastodon/features/collections/index.tsx
@@ -1,22 +1,16 @@
-import { useEffect, useMemo, useCallback, useId } from 'react';
+import { useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
-import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
-import { openModal } from 'mastodon/actions/modal';
-import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
-import { Dropdown } from 'mastodon/components/dropdown_menu';
import { Icon } from 'mastodon/components/icon';
-import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import ScrollableList from 'mastodon/components/scrollable_list';
import {
fetchAccountCollections,
@@ -24,119 +18,13 @@ import {
} from 'mastodon/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
+import { CollectionListItem } from './detail/collection_list_item';
import { messages as editorMessages } from './editor';
-import classes from './styles.module.scss';
const messages = defineMessages({
heading: { id: 'column.collections', defaultMessage: 'My collections' },
- view: {
- id: 'collections.view_collection',
- defaultMessage: 'View collection',
- },
- delete: {
- id: 'collections.delete_collection',
- defaultMessage: 'Delete collection',
- },
- more: { id: 'status.more', defaultMessage: 'More' },
});
-const CollectionItem: React.FC<{
- collection: ApiCollectionJSON;
-}> = ({ collection }) => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
-
- const { id, name } = collection;
-
- const handleDeleteClick = useCallback(() => {
- dispatch(
- openModal({
- modalType: 'CONFIRM_DELETE_COLLECTION',
- modalProps: {
- name,
- id,
- },
- }),
- );
- }, [dispatch, id, name]);
-
- const menu = useMemo(
- () => [
- { text: intl.formatMessage(messages.view), to: `/collections/${id}` },
- null,
- {
- text: intl.formatMessage(editorMessages.manageAccounts),
- to: `/collections/${id}/edit`,
- },
- {
- text: intl.formatMessage(editorMessages.editDetails),
- to: `/collections/${id}/edit/details`,
- },
- {
- text: intl.formatMessage(editorMessages.editSettings),
- to: `/collections/${id}/edit/settings`,
- },
- null,
- {
- text: intl.formatMessage(messages.delete),
- action: handleDeleteClick,
- dangerous: true,
- },
- ],
- [intl, id, handleDeleteClick],
- );
-
- const linkId = useId();
-
- return (
-
-
-
-
- {name}
-
-
-
-
-
- ),
- }}
- tagName='li'
- />
-
-
-
-
-
- );
-};
-
export const Collections: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
@@ -202,7 +90,7 @@ export const Collections: React.FC<{
bindToDocument={!multiColumn}
>
{collections.map((item) => (
-
+
))}
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 089e5764bc..5a9cebe5f4 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -65,6 +65,7 @@ import {
ListEdit,
ListMembers,
Collections,
+ CollectionDetail,
CollectionsEditor,
Blocks,
DomainBlocks,
@@ -269,12 +270,12 @@ class SwitchingColumnsArea extends PureComponent {
{areCollectionsEnabled() &&
-
+ [
+ ,
+ ,
+
+ ]
}
- {areCollectionsEnabled() &&
-
- }
-
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 6b0188f9b6..2beedaba26 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -44,13 +44,19 @@ export function Lists () {
return import('../../lists');
}
-export function Collections () {
+export function Collections() {
return import('../../collections').then(
module => ({default: module.Collections})
);
}
-export function CollectionsEditor () {
+export function CollectionDetail() {
+ return import('../../collections/detail/index').then(
+ module => ({default: module.CollectionDetailPage})
+ );
+}
+
+export function CollectionsEditor() {
return import('../../collections/editor').then(
module => ({default: module.CollectionEditorPage})
);
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 91e0fb79b2..9352ae5166 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -263,6 +263,11 @@
"collections.create_collection": "Create collection",
"collections.delete_collection": "Delete collection",
"collections.description_length_hint": "100 characters limit",
+ "collections.detail.accounts_heading": "Accounts",
+ "collections.detail.curated_by_author": "Curated by {author}",
+ "collections.detail.curated_by_you": "Curated by you",
+ "collections.detail.loading": "Loading collection…",
+ "collections.detail.share": "Share this collection",
"collections.edit_details": "Edit basic details",
"collections.edit_settings": "Edit settings",
"collections.error_loading_collections": "There was an error when trying to load your collections.",
diff --git a/app/javascript/material-icons/400-24px/more_vert-fill.svg b/app/javascript/material-icons/400-24px/more_vert-fill.svg
new file mode 100644
index 0000000000..e172f878a6
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/more_vert-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/more_vert.svg b/app/javascript/material-icons/400-24px/more_vert.svg
new file mode 100644
index 0000000000..e172f878a6
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/more_vert.svg
@@ -0,0 +1 @@
+
\ No newline at end of file