[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:
parent
1505cdd3a2
commit
b43005ac12
@ -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>
|
||||
))}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user