mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Update collection list item design (#38425)
This commit is contained in:
@@ -95,7 +95,7 @@ export const Avatar: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AvatarById: React.FC<
|
export const AvatarById: React.FC<
|
||||||
{ accountId: string } & Omit<Props, 'account'>
|
{ accountId: string | undefined } & Omit<Props, 'account'>
|
||||||
> = ({ accountId, ...otherProps }) => {
|
> = ({ accountId, ...otherProps }) => {
|
||||||
const account = useAccount(accountId);
|
const account = useAccount(accountId);
|
||||||
return <Avatar account={account} {...otherProps} />;
|
return <Avatar account={account} {...otherProps} />;
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Skeleton } from '../skeleton';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
import { DisplayNameWithoutDomain } from './no-domain';
|
import { DisplayNameWithoutDomain } from './no-domain';
|
||||||
|
|
||||||
export const DisplayNameDefault: FC<
|
export function useAccountHandle(
|
||||||
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
account: DisplayNameProps['account'],
|
||||||
> = ({ account, localDomain, className, ...props }) => {
|
localDomain: DisplayNameProps['localDomain'],
|
||||||
const username = useMemo(() => {
|
) {
|
||||||
|
return useMemo(() => {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,12 @@ export const DisplayNameDefault: FC<
|
|||||||
}
|
}
|
||||||
return `@${acct}`;
|
return `@${acct}`;
|
||||||
}, [account, localDomain]);
|
}, [account, localDomain]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayNameDefault: FC<
|
||||||
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
|
> = ({ account, localDomain, className, ...props }) => {
|
||||||
|
const username = useAccountHandle(account, localDomain);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisplayNameWithoutDomain
|
<DisplayNameWithoutDomain
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
collection={item}
|
collection={item}
|
||||||
withoutBorder={index === listedCollections.length - 1}
|
withoutBorder={index === listedCollections.length - 1}
|
||||||
|
withAuthorHandle={false}
|
||||||
positionInList={index + 1}
|
positionInList={index + 1}
|
||||||
listSize={listedCollections.length}
|
listSize={listedCollections.length}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
|
margin-inline: 24px;
|
||||||
|
padding-block: 12px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
&:not(.wrapperWithoutBorder) {
|
&:not(.wrapperWithoutBorder) {
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
@@ -12,12 +13,42 @@
|
|||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-block: 15px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarGrid {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, min-content);
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
&.avatarGridSensitive {
|
||||||
|
.avatar {
|
||||||
|
filter: blur(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
background: var(--color-bg-brand-softest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarSensitiveBadge {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
padding: 3px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background: var(--color-bg-warning-softest);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 2px;
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -40,15 +71,12 @@
|
|||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metaList {
|
.menuButton {
|
||||||
--gap: 0.75ch;
|
padding: 4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
|
||||||
display: flex;
|
svg {
|
||||||
flex-wrap: wrap;
|
width: 20px;
|
||||||
gap: var(--gap);
|
height: 20px;
|
||||||
|
|
||||||
& > li:not(:last-child)::after {
|
|
||||||
content: '·';
|
|
||||||
margin-inline-start: var(--gap);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,70 +5,65 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||||
|
import { AvatarById } from 'mastodon/components/avatar';
|
||||||
|
import { useAccountHandle } from 'mastodon/components/display_name/default';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import { Article } from 'mastodon/components/scrollable_list/components';
|
import { Article } from 'mastodon/components/scrollable_list/components';
|
||||||
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import classes from './collection_list_item.module.scss';
|
import classes from './collection_list_item.module.scss';
|
||||||
import { CollectionMenu } from './collection_menu';
|
import { CollectionMenu } from './collection_menu';
|
||||||
|
|
||||||
export const CollectionMetaData: React.FC<{
|
export const AvatarGrid: React.FC<{
|
||||||
collection: ApiCollectionJSON;
|
accountIds: (string | undefined)[];
|
||||||
extended?: boolean;
|
sensitive?: boolean;
|
||||||
className?: string;
|
}> = ({ accountIds: ids, sensitive }) => {
|
||||||
}> = ({ collection, extended, className }) => {
|
const avatarIds = [ids[0], ids[1], ids[2], ids[3]];
|
||||||
return (
|
return (
|
||||||
<ul className={classNames(classes.metaList, className)}>
|
<div
|
||||||
<FormattedMessage
|
className={classNames(
|
||||||
id='collections.account_count'
|
classes.avatarGrid,
|
||||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
sensitive ? classes.avatarGridSensitive : null,
|
||||||
values={{ count: collection.item_count }}
|
|
||||||
tagName='li'
|
|
||||||
/>
|
|
||||||
{extended && (
|
|
||||||
<>
|
|
||||||
{collection.discoverable ? (
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.visibility_public'
|
|
||||||
defaultMessage='Public'
|
|
||||||
tagName='li'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.visibility_unlisted'
|
|
||||||
defaultMessage='Unlisted'
|
|
||||||
tagName='li'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{collection.sensitive && (
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.sensitive'
|
|
||||||
defaultMessage='Sensitive'
|
|
||||||
tagName='li'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<FormattedMessage
|
>
|
||||||
id='collections.last_updated_at'
|
{avatarIds.map((id) => (
|
||||||
defaultMessage='Last updated: {date}'
|
<AvatarById
|
||||||
values={{
|
animate={false}
|
||||||
date: <RelativeTimestamp timestamp={collection.updated_at} long />,
|
key={id}
|
||||||
}}
|
accountId={id}
|
||||||
tagName='li'
|
className={classes.avatar}
|
||||||
/>
|
size={25}
|
||||||
</ul>
|
/>
|
||||||
|
))}
|
||||||
|
{sensitive && <WarningIcon className={classes.avatarSensitiveBadge} />}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CollectionListItem: React.FC<{
|
export const CollectionListItem: React.FC<{
|
||||||
collection: ApiCollectionJSON;
|
collection: ApiCollectionJSON;
|
||||||
withoutBorder?: boolean;
|
withoutBorder?: boolean;
|
||||||
|
withAuthorHandle?: boolean;
|
||||||
|
withTimestamp?: boolean;
|
||||||
positionInList: number;
|
positionInList: number;
|
||||||
listSize: number;
|
listSize: number;
|
||||||
}> = ({ collection, withoutBorder, positionInList, listSize }) => {
|
}> = ({
|
||||||
|
collection,
|
||||||
|
withoutBorder,
|
||||||
|
withAuthorHandle = true,
|
||||||
|
withTimestamp,
|
||||||
|
positionInList,
|
||||||
|
listSize,
|
||||||
|
}) => {
|
||||||
const { id, name } = collection;
|
const { id, name } = collection;
|
||||||
const linkId = useId();
|
const uniqueId = useId();
|
||||||
|
const linkId = `${uniqueId}-link`;
|
||||||
|
const infoId = `${uniqueId}-info`;
|
||||||
|
const authorAccount = useAccount(collection.account_id);
|
||||||
|
const authorHandle = useAccountHandle(authorAccount, domain);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Article
|
<Article
|
||||||
@@ -78,19 +73,67 @@ export const CollectionListItem: React.FC<{
|
|||||||
withoutBorder && classes.wrapperWithoutBorder,
|
withoutBorder && classes.wrapperWithoutBorder,
|
||||||
)}
|
)}
|
||||||
aria-labelledby={linkId}
|
aria-labelledby={linkId}
|
||||||
|
aria-describedby={infoId}
|
||||||
aria-posinset={positionInList}
|
aria-posinset={positionInList}
|
||||||
aria-setsize={listSize}
|
aria-setsize={listSize}
|
||||||
>
|
>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<h2 id={linkId}>
|
<AvatarGrid
|
||||||
<Link to={`/collections/${id}`} className={classes.link}>
|
accountIds={collection.items.map((item) => item.account_id)}
|
||||||
{name}
|
sensitive={collection.sensitive}
|
||||||
</Link>
|
/>
|
||||||
</h2>
|
<div>
|
||||||
<CollectionMetaData collection={collection} className={classes.info} />
|
<h2 id={linkId}>
|
||||||
|
<Link to={`/collections/${id}`} className={classes.link}>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<ul className={classes.info} id={infoId}>
|
||||||
|
{collection.sensitive && (
|
||||||
|
<li className='sr-only'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.sensitive'
|
||||||
|
defaultMessage='Sensitive'
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{withAuthorHandle && authorAccount && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.by_account'
|
||||||
|
defaultMessage='by {account_handle}'
|
||||||
|
values={{
|
||||||
|
account_handle: authorHandle,
|
||||||
|
}}
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.account_count'
|
||||||
|
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||||
|
values={{ count: collection.item_count }}
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
{withTimestamp && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.last_updated_at'
|
||||||
|
defaultMessage='Last updated: {date}'
|
||||||
|
values={{
|
||||||
|
date: (
|
||||||
|
<RelativeTimestamp timestamp={collection.updated_at} long />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CollectionMenu context='list' collection={collection} />
|
<CollectionMenu
|
||||||
|
context='list'
|
||||||
|
collection={collection}
|
||||||
|
className={classes.menuButton}
|
||||||
|
/>
|
||||||
</Article>
|
</Article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { useHistory, useLocation, useParams } from 'react-router';
|
import { useHistory, useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
|
import { RelativeTimestamp } from '@/mastodon/components/relative_timestamp';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||||
@@ -25,7 +26,6 @@ import { fetchCollection } from 'mastodon/reducers/slices/collections';
|
|||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { CollectionAccountsList } from './accounts_list';
|
import { CollectionAccountsList } from './accounts_list';
|
||||||
import { CollectionMetaData } from './collection_list_item';
|
|
||||||
import { CollectionMenu } from './collection_menu';
|
import { CollectionMenu } from './collection_menu';
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
@@ -40,6 +40,54 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CollectionMetaData: React.FC<{
|
||||||
|
collection: ApiCollectionJSON;
|
||||||
|
extended?: boolean;
|
||||||
|
}> = ({ collection, extended }) => {
|
||||||
|
return (
|
||||||
|
<ul className={classes.metaList}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.account_count'
|
||||||
|
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||||
|
values={{ count: collection.item_count }}
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
{extended && (
|
||||||
|
<>
|
||||||
|
{collection.discoverable ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.visibility_public'
|
||||||
|
defaultMessage='Public'
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.visibility_unlisted'
|
||||||
|
defaultMessage='Unlisted'
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{collection.sensitive && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.sensitive'
|
||||||
|
defaultMessage='Sensitive'
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.last_updated_at'
|
||||||
|
defaultMessage='Last updated: {date}'
|
||||||
|
values={{
|
||||||
|
date: <RelativeTimestamp timestamp={collection.updated_at} long />,
|
||||||
|
}}
|
||||||
|
tagName='li'
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
|
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
|
||||||
id,
|
id,
|
||||||
// When previewMode is enabled, your own display name
|
// When previewMode is enabled, your own display name
|
||||||
@@ -137,7 +185,6 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
|||||||
<CollectionMetaData
|
<CollectionMetaData
|
||||||
extended={account_id === me}
|
extended={account_id === me}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
className={classes.metaData}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,9 +52,19 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metaData {
|
.metaList {
|
||||||
|
--gap: 0.75ch;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
gap: var(--gap);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
& > li:not(:last-child)::after {
|
||||||
|
content: '·';
|
||||||
|
margin-inline-start: var(--gap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.columnSubheading {
|
.columnSubheading {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export const Collections: React.FC<{
|
|||||||
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
||||||
{collections.map((item, index) => (
|
{collections.map((item, index) => (
|
||||||
<CollectionListItem
|
<CollectionListItem
|
||||||
|
withTimestamp
|
||||||
|
withAuthorHandle={false}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
collection={item}
|
collection={item}
|
||||||
positionInList={index + 1}
|
positionInList={index + 1}
|
||||||
|
|||||||
@@ -359,6 +359,7 @@
|
|||||||
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
|
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
|
||||||
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
|
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
|
||||||
"collections.accounts.empty_title": "This collection is empty",
|
"collections.accounts.empty_title": "This collection is empty",
|
||||||
|
"collections.by_account": "by {account_handle}",
|
||||||
"collections.collection_description": "Description",
|
"collections.collection_description": "Description",
|
||||||
"collections.collection_language": "Language",
|
"collections.collection_language": "Language",
|
||||||
"collections.collection_language_none": "None",
|
"collections.collection_language_none": "None",
|
||||||
|
|||||||
@@ -4099,11 +4099,10 @@ a.account__display-name {
|
|||||||
.column-subheading {
|
.column-subheading {
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
padding: 8px 20px;
|
padding: 12px 24px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.getting-started__wrapper {
|
.getting-started__wrapper {
|
||||||
|
|||||||
Reference in New Issue
Block a user