diff --git a/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx b/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx index 05789ff85c..b706dc90ba 100644 --- a/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx +++ b/app/javascript/flavours/glitch/features/collections/detail/accounts_list.tsx @@ -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 ; -}; - 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={ + + + } + hint={intl.formatNumber(account.followers_count)} + > + + + + + } + hint={intl.formatNumber(account.statuses_count)} + > + + + + + } + > + + + + } /> {!withoutButton && } - - ); -}; - -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 ( -
- - + {isOwnAccount && ( + + )}
); }; @@ -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(null); const isOwnCollection = collection?.account_id === me; - const { items, currentUserInCollection } = getCollectionItems(collection); + const { items = [] } = collection ?? {}; return ( - {collection && currentUserInCollection ? ( - <> -

- , - }} - /> -

-
- - -
-

- -

- - ) : ( -

- {collection ? ( - - ) : ( - - )} -

- )} +

+ {collection ? ( + + ) : ( + + )} +

{collection && ( - {items.map(({ account_id }, index, items) => ( + {items.map(({ account_id }, index) => (
))} diff --git a/app/javascript/flavours/glitch/features/collections/detail/index.tsx b/app/javascript/flavours/glitch/features/collections/detail/index.tsx index 157bcbbc31..28e67837de 100644 --- a/app/javascript/flavours/glitch/features/collections/detail/index.tsx +++ b/app/javascript/flavours/glitch/features/collections/detail/index.tsx @@ -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 ( + + } + primaryLabel={ + + } + onPrimary={confirmRevoke} + > + , + date: '{date}', // TODO: Data not yet provided by API + }} + /> + + ); +}; + 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 }> = ({ {description &&

{description}

} + {isCurrentUserInCollection && } ); }; diff --git a/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx b/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx index cf7eac6a0c..d75f41751e 100644 --- a/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx +++ b/app/javascript/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal.tsx @@ -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; diff --git a/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss b/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss index 62e2285294..54d421e037 100644 --- a/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss +++ b/app/javascript/flavours/glitch/features/collections/detail/styles.module.scss @@ -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; - } -}