import { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; import type { Map as ImmutableMap } from 'immutable'; import ArticleIcon from '@/material-icons/400-24px/article.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import { Icon } from 'mastodon/components/icon'; import StatusContainer from 'mastodon/containers/status_container'; import type { Status } from 'mastodon/models/status'; import type { RootState } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store'; import QuoteIcon from '../../images/quote.svg?react'; import { makeGetStatus } from '../selectors'; const MAX_QUOTE_POSTS_NESTING_LEVEL = 1; const QuoteWrapper: React.FC<{ isError?: boolean; children: React.ReactElement; }> = ({ isError, children }) => { return (
{children}
); }; const QuoteLink: React.FC<{ status: Status; }> = ({ status }) => { const accountId = status.get('account') as string; const account = useAppSelector((state) => accountId ? state.accounts.get(accountId) : undefined, ); const quoteAuthorName = account?.display_name_html; if (!quoteAuthorName) { return null; } const quoteAuthorElement = ( ); const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; return ( ); }; type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>; type GetStatusSelector = ( state: RootState, props: { id?: string | null; contextType?: string }, ) => Status | null; export const QuotedStatus: React.FC<{ quote: QuoteMap; contextType?: string; variant?: 'full' | 'link'; nestingLevel?: number; }> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => { const quotedStatusId = quote.get('quoted_status'); const quoteState = quote.get('state'); const status = useAppSelector((state) => quotedStatusId ? state.statuses.get(quotedStatusId) : undefined, ); let quoteError: React.ReactNode = null; // In order to find out whether the quoted post should be completely hidden // due to a matching filter, we run it through the selector used by `status_container`. // If this returns null even though `status` exists, it's because it's filtered. const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector; const statusWithExtraData = useAppSelector((state) => getStatus(state, { id: quotedStatusId, contextType }), ); const isFilteredAndHidden = status && statusWithExtraData === null; if (isFilteredAndHidden) { quoteError = ( ); } else if (quoteState === 'deleted') { quoteError = ( ); } else if (quoteState === 'unauthorized') { quoteError = ( ); } else if (quoteState === 'pending') { quoteError = ( ); } else if (quoteState === 'rejected' || quoteState === 'revoked') { quoteError = ( ); } else if (!status || !quotedStatusId) { quoteError = ( ); } if (quoteError) { return {quoteError}; } if (variant === 'link' && status) { return ; } const childQuote = status?.get('quote') as QuoteMap | undefined; const canRenderChildQuote = childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL; return ( {/* @ts-expect-error Status is not yet typed */} {canRenderChildQuote && ( )} ); }; interface StatusQuoteManagerProps { id: string; contextType?: string; [key: string]: unknown; } /** * This wrapper component takes a status ID and, if the associated status * is a quote post, it renders the quote into `StatusContainer` as a child. * It passes all other props through to `StatusContainer`. */ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => { const status = useAppSelector((state) => { const status = state.statuses.get(props.id); const reblogId = status?.get('reblog') as string | undefined; return reblogId ? state.statuses.get(reblogId) : status; }); const quote = status?.get('quote') as QuoteMap | undefined; if (quote) { return ( ); } return ; };