Echo e4c8ac82bd [Glitch] Swap from React Helmet to Unhead
Port d0c8eb2f1b37c8952987172c93815f58a63d8e80 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2026-05-07 18:30:21 +02:00

260 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect } 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 'flavours/glitch/actions/modal';
import type {
ApiCollectionJSON,
CollectionAccountItem,
} from 'flavours/glitch/api_types/collections';
import { Badge } from 'flavours/glitch/components/badge';
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 { useAccountHandle } from 'flavours/glitch/components/display_name/default';
import { FormattedDateWrapper } from 'flavours/glitch/components/formatted_date';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { Scrollable } from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { domain, me } from 'flavours/glitch/initial_state';
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/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 = (
<Link to={`/@${account.acct}`} data-hover-card-account={account.id}>
{authorHandle}
</Link>
);
return (
<p className={classes.authorNote}>
<FormattedMessage
id='collections.by_account'
defaultMessage='by {account_handle}'
values={{
account_handle: author,
}}
/>
</p>
);
};
const RevokeControls: React.FC<{
currentUserCollectionItem: CollectionAccountItem;
collection: ApiCollectionJSON;
}> = ({ currentUserCollectionItem, collection }) => {
const authorAccount = useAccount(collection.account_id);
const confirmRevoke = useConfirmRevoke(collection);
return (
<Callout
icon={StarIcon}
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: (
<FormattedDateWrapper
value={currentUserCollectionItem.created_at}
day='2-digit'
month='short'
year='numeric'
/>
),
}}
/>
</Callout>
);
};
export const PendingNote: React.FC = () => {
return (
<Callout
variant='subtle'
icon={HelpIcon}
title={
<FormattedMessage
id='collections.pending_accounts.title'
defaultMessage='Why am I seeing pending accounts?'
/>
}
>
<FormattedMessage
id='collections.pending_accounts.message'
defaultMessage='Accounts may appear as pending when were awaiting a response from the user or their server. Only you can see pending accounts.'
/>
</Callout>
);
};
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
}) => {
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 (
<header className={classes.header}>
<div className={classes.titleWithMenu}>
<div className={classes.titleWrapper}>
{tag && <Badge label={`#${tag.name}`} icon={null} />}
<h2 className={classes.name}>{name}</h2>
<AuthorNote id={account_id} />
</div>
<div className={classes.headerButtonWrapper}>
<IconButton
iconComponent={ShareIcon}
icon='share-icon'
title={intl.formatMessage(messages.share)}
className={classes.iconButton}
onClick={openShareModal}
/>
<CollectionMenu
context='collection'
collection={collection}
className={classes.iconButton}
/>
</div>
</div>
{description && <p className={classes.description}>{description}</p>}
{hasPendingAccounts && <PendingNote />}
{isCurrentUserInCollection && (
<RevokeControls
currentUserCollectionItem={currentUserCollectionItem}
collection={collection}
/>
)}
</header>
);
};
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 (
<Column bindToDocument={!multiColumn} label={pageTitle}>
<ColumnHeader
showBackButton
title={pageTitle}
icon='collection-icon'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
/>
<Scrollable>
{collection ? (
<>
<CollectionHeader collection={collection} />
<CollectionAccountsList collection={collection} />
</>
) : (
<LoadingIndicator />
)}
</Scrollable>
<Helmet>
<title>{pageTitle}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};