import { useCallback, useEffect, useRef, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useLocation, useParams } from 'react-router'; import { Link } from 'react-router-dom'; import { Helmet } from '@unhead/react/helmet'; import HelpIcon from '@/material-icons/400-24px/help.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import StarIcon from '@/material-icons/400-24px/star.svg?react'; import { openModal } from 'mastodon/actions/modal'; import type { ApiCollectionJSON, CollectionAccountItem, } from 'mastodon/api_types/collections'; import { Badge } from 'mastodon/components/badge'; import { Callout } from 'mastodon/components/callout'; import { Column } from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; import { DisplayName } from 'mastodon/components/display_name'; import { useAccountHandle } from 'mastodon/components/display_name/default'; import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { IconButton } from 'mastodon/components/icon_button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { Scrollable } from 'mastodon/components/scrollable_list/components'; import { useAccount } from 'mastodon/hooks/useAccount'; import { domain, me } from 'mastodon/initial_state'; import { fetchCollection } from 'mastodon/reducers/slices/collections'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { CollectionMenu } from '../components/collection_menu'; import { CollectionAccountsList } from './accounts_list'; import { useConfirmRevoke } from './revoke_collection_inclusion_modal'; import classes from './styles.module.scss'; const messages = defineMessages({ loading: { id: 'collections.detail.loading', defaultMessage: 'Loading collection…', }, share: { id: 'collections.detail.share', defaultMessage: 'Share this collection', }, }); export const AuthorNote: React.FC<{ id: string }> = ({ id }) => { const account = useAccount(id); const authorHandle = useAccountHandle(account, domain); if (!account) { return null; } const author = ( {authorHandle} ); return (

); }; const RevokeControls: React.FC<{ currentUserCollectionItem: CollectionAccountItem; collection: ApiCollectionJSON; }> = ({ currentUserCollectionItem, collection }) => { const authorAccount = useAccount(collection.account_id); const confirmRevoke = useConfirmRevoke(collection); return ( } primaryLabel={ } onPrimary={confirmRevoke} > , date: ( ), }} /> ); }; export const PendingNote: React.FC = () => { return ( } > ); }; const SensitiveContentNote: React.FC<{ onReveal: () => void }> = ({ onReveal, }) => { return ( } primaryLabel={ } onPrimary={onReveal} className={classes.sensitiveScreen} > ); }; const CollectionHeader: React.FC<{ collection: ApiCollectionJSON; withDescription: boolean; headingRef: React.RefObject; }> = ({ collection, withDescription, headingRef }) => { const intl = useIntl(); const { name, description, tag, account_id, items } = collection; const dispatch = useAppDispatch(); const history = useHistory(); const isOwnCollection = account_id === me; const currentUserCollectionItem = items.find( (account) => account.account_id === me, ); const isCurrentUserInCollection = !isOwnCollection && !!currentUserCollectionItem; const openShareModal = useCallback(() => { dispatch( openModal({ modalType: 'SHARE_COLLECTION', modalProps: { collection, }, }), ); }, [collection, dispatch]); const location = useLocation<{ newCollection?: boolean } | undefined>(); const isNewCollection = location.state?.newCollection; useEffect(() => { if (isNewCollection) { // Replace with current pathname to clear `newCollection` state history.replace(location.pathname); openShareModal(); } }, [history, openShareModal, isNewCollection, location.pathname]); const hasPendingAccounts = items.some((item) => item.state === 'pending'); return (
{tag && }

{name}

{withDescription && description && (

{description}

)} {hasPendingAccounts && } {isCurrentUserInCollection && ( )}
); }; function useRevealSensitiveContent({ sensitive, }: { sensitive: boolean | undefined; }) { const postRevealFocusTargetRef = useRef(null); const [isContentVisible, setIsContentVisible] = useState(!sensitive); const revealContent = useCallback(() => { setIsContentVisible(true); setTimeout(() => { postRevealFocusTargetRef.current?.focus(); }, 0); }, [postRevealFocusTargetRef]); return { isContentVisible, revealContent, postRevealFocusTargetRef, }; } const ColumnContent: React.FC<{ collection: ApiCollectionJSON; }> = ({ collection }) => { const { isContentVisible, revealContent, postRevealFocusTargetRef } = useRevealSensitiveContent({ sensitive: collection.sensitive && collection.account_id !== me, }); return ( <> {isContentVisible ? ( ) : ( )} ); }; 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, ); useEffect(() => { if (id) { void dispatch(fetchCollection({ collectionId: id })); } }, [dispatch, id]); const pageTitle = collection?.name ?? intl.formatMessage(messages.loading); return ( {collection ? ( ) : ( )} {pageTitle} ); };