[Glitch] Update collection account list design

Port 225069d1f2161c94c696c74405ce2a058154bafc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion 2026-04-02 15:32:05 +02:00 committed by Claire
parent 1505cdd3a2
commit b43005ac12
4 changed files with 183 additions and 183 deletions

View File

@ -2,26 +2,26 @@ import { useCallback, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Callout } from '@/flavours/glitch/components/callout';
import { FollowButton } from '@/flavours/glitch/components/follow_button';
import { openModal } from 'flavours/glitch/actions/modal';
import type {
ApiCollectionJSON,
CollectionAccountItem,
} from 'flavours/glitch/api_types/collections';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Account } from 'flavours/glitch/components/account';
import { Button } from 'flavours/glitch/components/button';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { Callout } from 'flavours/glitch/components/callout';
import { FollowButton } from 'flavours/glitch/components/follow_button';
import {
NumberFields,
NumberFieldsItem,
} from 'flavours/glitch/components/number_fields';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import {
Article,
ItemList,
} from 'flavours/glitch/components/scrollable_list/components';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { useDismissible } from 'flavours/glitch/hooks/useDismissible';
import { useRelationship } from 'flavours/glitch/hooks/useRelationship';
import { me } from 'flavours/glitch/initial_state';
import { useAppDispatch } from 'flavours/glitch/store';
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
import classes from './styles.module.scss';
const messages = defineMessages({
@ -31,28 +31,33 @@ const messages = defineMessages({
},
});
const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
const account = useAccount(id);
return <DisplayName account={account} variant='simple' />;
};
const AccountItem: React.FC<{
accountId: string | undefined;
collectionOwnerId: string;
onRevoke: () => void;
withBio?: boolean;
withBorder?: boolean;
}> = ({ accountId, withBio = true, withBorder = true, collectionOwnerId }) => {
}> = ({
accountId,
collectionOwnerId,
onRevoke,
withBio = true,
withBorder = true,
}) => {
const intl = useIntl();
const account = useAccount(accountId);
const relationship = useRelationship(accountId);
if (!accountId) {
if (!accountId || !account) {
return null;
}
// When viewing your own collection, only show the Follow button
// for accounts you're not following (anymore).
// Otherwise, always show the follow button in its various states.
const isOwnAccount = accountId === me;
const withoutButton =
accountId === me ||
isOwnAccount ||
!relationship ||
(collectionOwnerId === me &&
(relationship.following || relationship.requested));
@ -66,52 +71,55 @@ const AccountItem: React.FC<{
withBorder={false}
withMenu={false}
className={classes.accountItem}
extraAccountInfo={
<NumberFields>
<NumberFieldsItem
label={
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
}
hint={intl.formatNumber(account.followers_count)}
>
<ShortNumber value={account.followers_count} />
</NumberFieldsItem>
<NumberFieldsItem
label={
<FormattedMessage id='account.posts' defaultMessage='Posts' />
}
hint={intl.formatNumber(account.statuses_count)}
>
<ShortNumber value={account.statuses_count} />
</NumberFieldsItem>
<NumberFieldsItem
label={
<FormattedMessage
id='account.last_active'
defaultMessage='Last active'
/>
}
>
<RelativeTimestamp
long
timestamp={account.last_status_at}
noFuture
/>
</NumberFieldsItem>
</NumberFields>
}
/>
{!withoutButton && <FollowButton accountId={accountId} />}
</div>
);
};
const RevokeControls: React.FC<{
collectionId: string;
collectionItem: CollectionAccountItem;
}> = ({ collectionId, collectionItem }) => {
const dispatch = useAppDispatch();
const confirmRevoke = useCallback(() => {
void dispatch(
openModal({
modalType: 'REVOKE_COLLECTION_INCLUSION',
modalProps: {
collectionId,
collectionItemId: collectionItem.id,
},
}),
);
}, [collectionId, collectionItem.id, dispatch]);
const { wasDismissed, dismiss } = useDismissible(
`collection-revoke-hint-${collectionItem.id}`,
);
if (wasDismissed) {
return null;
}
return (
<div className={classes.revokeControlWrapper}>
<Button secondary onClick={dismiss}>
<FormattedMessage
id='collections.detail.accept_inclusion'
defaultMessage='Okay'
/>
</Button>
<Button secondary onClick={confirmRevoke}>
<FormattedMessage
id='collections.detail.revoke_inclusion'
defaultMessage='Remove me'
/>
</Button>
{isOwnAccount && (
<Button secondary onClick={onRevoke}>
<FormattedMessage
id='collections.detail.revoke_inclusion'
defaultMessage='Remove me'
/>
</Button>
)}
</div>
);
};
@ -159,46 +167,16 @@ const SensitiveScreen: React.FC<{
);
};
/**
* Returns the collection's account items. If the current user's account
* is part of the collection, it will be returned separately.
*/
function getCollectionItems(collection: ApiCollectionJSON | undefined) {
if (!collection)
return {
currentUserInCollection: null,
items: [],
};
const { account_id, items } = collection;
const isOwnCollection = account_id === me;
const currentUserIndex = items.findIndex(
(account) => account.account_id === me,
);
if (isOwnCollection || currentUserIndex === -1) {
return {
currentUserInCollection: null,
items,
};
} else {
return {
currentUserInCollection: items.at(currentUserIndex) ?? null,
items: items.toSpliced(currentUserIndex, 1),
};
}
}
export const CollectionAccountsList: React.FC<{
collection?: ApiCollectionJSON;
isLoading: boolean;
}> = ({ collection, isLoading }) => {
const intl = useIntl();
const confirmRevoke = useConfirmRevoke(collection);
const listHeadingRef = useRef<HTMLHeadingElement>(null);
const isOwnCollection = collection?.account_id === me;
const { items, currentUserInCollection } = getCollectionItems(collection);
const { items = [] } = collection ?? {};
return (
<ItemList
@ -206,80 +184,40 @@ export const CollectionAccountsList: React.FC<{
emptyMessage={intl.formatMessage(messages.empty)}
className={classes.itemList}
>
{collection && currentUserInCollection ? (
<>
<h3 className={classes.columnSubheading}>
<FormattedMessage
id='collections.detail.you_were_added_to_this_collection'
defaultMessage='You were added to this collection'
values={{
author: <SimpleAuthorName id={collection.account_id} />,
}}
/>
</h3>
<Article
key={currentUserInCollection.account_id}
aria-posinset={1}
aria-setsize={items.length}
className={classes.youWereAddedWrapper}
>
<AccountItem
withBorder={false}
withBio={false}
accountId={currentUserInCollection.account_id}
collectionOwnerId={collection.account_id}
/>
<RevokeControls
collectionId={collection.id}
collectionItem={currentUserInCollection}
/>
</Article>
<h3
className={classes.columnSubheading}
tabIndex={-1}
ref={listHeadingRef}
>
<FormattedMessage
id='collections.detail.other_accounts_count'
defaultMessage='{count, plural, one {# other account} other {# other accounts}}'
values={{ count: collection.item_count - 1 }}
/>
</h3>
</>
) : (
<h3
className={classes.columnSubheading}
tabIndex={-1}
ref={listHeadingRef}
>
{collection ? (
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
/>
) : (
<FormattedMessage
id='collections.detail.accounts_heading'
defaultMessage='Accounts'
/>
)}
</h3>
)}
<h3
className={classes.columnSubheading}
tabIndex={-1}
ref={listHeadingRef}
>
{collection ? (
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
/>
) : (
<FormattedMessage
id='collections.detail.accounts_heading'
defaultMessage='Accounts'
/>
)}
</h3>
{collection && (
<SensitiveScreen
sensitive={!isOwnCollection && collection.sensitive}
focusTargetRef={listHeadingRef}
>
{items.map(({ account_id }, index, items) => (
{items.map(({ account_id }, index) => (
<Article
key={account_id}
aria-posinset={index + (currentUserInCollection ? 2 : 1)}
aria-posinset={index + 1}
aria-setsize={items.length}
>
<AccountItem
withBorder={index !== items.length - 1}
accountId={account_id}
collectionOwnerId={collection.account_id}
onRevoke={confirmRevoke}
/>
</Article>
))}

View File

@ -11,17 +11,20 @@ import { useAccountHandle } from '@/flavours/glitch/components/display_name/defa
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Callout } from 'flavours/glitch/components/callout';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { domain } from 'flavours/glitch/initial_state';
import { domain, me } from 'flavours/glitch/initial_state';
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollectionAccountsList } from './accounts_list';
import { CollectionMenu } from './collection_menu';
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
import classes from './styles.module.scss';
const messages = defineMessages({
@ -62,14 +65,54 @@ export const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
);
};
export const RevokeControls: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
const authorAccount = useAccount(collection.account_id);
const confirmRevoke = useConfirmRevoke(collection);
return (
<Callout
title={
<FormattedMessage
id='collections.detail.you_are_in_this_collection'
defaultMessage="You're featured in this collection"
/>
}
primaryLabel={
<FormattedMessage
id='collections.detail.revoke_inclusion'
defaultMessage='Remove me'
/>
}
onPrimary={confirmRevoke}
>
<FormattedMessage
id='collections.detail.author_added_you_on_date'
defaultMessage='{author} added you on {date}'
values={{
author: <DisplayName account={authorAccount} variant='simple' />,
date: '{date}', // TODO: Data not yet provided by API
}}
/>
</Callout>
);
};
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
}) => {
const intl = useIntl();
const { name, description, tag, account_id } = collection;
const { name, description, tag, account_id, items } = collection;
const dispatch = useAppDispatch();
const history = useHistory();
const isOwnCollection = account_id === me;
const currentUserIndex = items.findIndex(
(account) => account.account_id === me,
);
const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1;
const handleShare = useCallback(() => {
dispatch(
openModal({
@ -115,6 +158,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
</div>
</div>
{description && <p className={classes.description}>{description}</p>}
{isCurrentUserInCollection && <RevokeControls collection={collection} />}
</header>
);
};

View File

@ -3,8 +3,11 @@ import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { showAlert } from 'flavours/glitch/actions/alerts';
import { openModal } from 'flavours/glitch/actions/modal';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import type { BaseConfirmationModalProps } from 'flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal';
import { ConfirmationModal } from 'flavours/glitch/features/ui/components/confirmation_modals/confirmation_modal';
import { me } from 'flavours/glitch/initial_state';
import { revokeCollectionInclusion } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
@ -24,6 +27,24 @@ const messages = defineMessages({
},
});
export function useConfirmRevoke(collection?: ApiCollectionJSON) {
const dispatch = useAppDispatch();
const { id, items = [] } = collection ?? {};
const ownCollectionItemId = items.find((item) => item.account_id === me)?.id;
return useCallback(() => {
void dispatch(
openModal({
modalType: 'REVOKE_COLLECTION_INCLUSION',
modalProps: {
collectionId: id,
collectionItemId: ownCollectionItemId,
},
}),
);
}, [dispatch, id, ownCollectionItemId]);
}
export const RevokeCollectionInclusionModal: React.FC<
{
collectionId: string;

View File

@ -1,4 +1,7 @@
.header {
display: flex;
flex-direction: column;
gap: 12px;
padding: 24px;
}
@ -49,7 +52,6 @@
.description {
font-size: 15px;
margin-top: 12px;
}
.headerButtonWrapper {
@ -59,8 +61,14 @@
.iconButton {
box-sizing: content-box;
padding: 5px;
padding: 7px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
svg {
width: 20px;
height: 20px;
}
}
.itemList {
@ -81,6 +89,18 @@
&[data-with-border='true'] {
border-bottom: 1px solid var(--color-border-primary);
}
:global(.account__note) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
}
// Hide 'No description provided' message added by `Account` component
:global(.account__note--missing) {
display: none;
}
}
.accountItem {
@ -93,26 +113,3 @@
flex-grow: 1;
}
.youWereAddedWrapper {
padding-bottom: 16px;
}
.revokeControlWrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-bottom: 8px;
:global(.button) {
min-width: 30%;
white-space: normal;
}
--avatar-width: 46px;
@container (width < 360px) {
--avatar-width: 35px;
}
}