import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import type { Map as ImmutableMap } from 'immutable'; import { LearnMoreLink } from 'flavours/glitch/components/learn_more_link'; import StatusContainer from 'flavours/glitch/containers/status_container'; import { domain } from 'flavours/glitch/initial_state'; import type { Account } from 'flavours/glitch/models/account'; import type { Status } from 'flavours/glitch/models/status'; import type { RootState } from 'flavours/glitch/store'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; import { fetchRelationships } from '../actions/accounts'; import { revealAccount } from '../actions/accounts_typed'; import { fetchStatus } from '../actions/statuses'; import { makeGetStatusWithExtraInfo } from '../selectors'; import { getAccountHidden } from '../selectors/accounts'; import { Button } from './button'; const MAX_QUOTE_POSTS_NESTING_LEVEL = 1; const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => { const accountObjectOrId = status.get('account') as string | Account; const accountId = typeof accountObjectOrId === 'string' ? accountObjectOrId : accountObjectOrId.id; const account = useAppSelector((state) => accountId ? state.accounts.get(accountId) : undefined, ); const quoteAuthorName = account?.acct; if (!quoteAuthorName) { return null; } return (
); }; type GetStatusSelector = ( state: RootState, props: { id?: string | null; contextType?: string }, ) => { status: Status | null; loadingState: 'not-found' | 'loading' | 'filtered' | 'complete'; }; type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>; const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => { const dispatch = useAppDispatch(); const reveal = useCallback(() => { dispatch(revealAccount({ id: accountId })); }, [dispatch, accountId]); return ( <> ); }; const FilteredQuote: React.FC<{ reveal: VoidFunction; quotedAccountId: string; quoteState: string; }> = ({ reveal, quotedAccountId, quoteState }) => { const account = useAppSelector((state) => quotedAccountId ? state.accounts.get(quotedAccountId) : undefined, ); const quoteAuthorName = account?.acct; const domain = quoteAuthorName?.split('@')[1]; let message; switch (quoteState) { case 'blocked_account': message = ( ); break; case 'blocked_domain': message = ( ); break; case 'muted_account': message = ( ); } return ( <> {message} ); }; interface QuotedStatusProps { quote: QuoteMap; contextType?: string; parentQuotePostId?: string | null; variant?: 'full' | 'link'; nestingLevel?: number; onQuoteCancel?: () => void; // Used for composer. } export const QuotedStatus: React.FC = ({ quote, contextType, parentQuotePostId, nestingLevel = 1, variant = 'full', onQuoteCancel, }) => { const dispatch = useAppDispatch(); const quoteState = useAppSelector((state) => parentQuotePostId ? state.statuses.getIn([parentQuotePostId, 'quote', 'state']) : quote.get('state'), ); const quotedStatusId = quote.get('quoted_status'); const getStatusSelector = useMemo( () => makeGetStatusWithExtraInfo() as GetStatusSelector, [], ); const { status, loadingState } = useAppSelector((state) => getStatusSelector(state, { id: quotedStatusId, contextType }), ); const accountId: string | null = status?.get('account') ? (status.get('account') as Account).id : null; const hiddenAccount = useAppSelector( (state) => accountId && getAccountHidden(state, accountId), ); const shouldFetchQuote = !status?.get('isLoading') && quoteState !== 'deleted' && loadingState === 'not-found'; const isLoaded = loadingState === 'complete'; const isFetchingQuoteRef = useRef(false); const [revealed, setRevealed] = useState(false); const reveal = useCallback(() => { setRevealed(true); }, [setRevealed]); useEffect(() => { if (isLoaded) { isFetchingQuoteRef.current = false; } }, [isLoaded]); useEffect(() => { if (shouldFetchQuote && quotedStatusId && !isFetchingQuoteRef.current) { dispatch( fetchStatus(quotedStatusId, { parentQuotePostId, alsoFetchContext: false, }), ); isFetchingQuoteRef.current = true; } }, [shouldFetchQuote, quotedStatusId, parentQuotePostId, dispatch]); useEffect(() => { if (accountId && hiddenAccount) dispatch(fetchRelationships([accountId])); }, [accountId, hiddenAccount, dispatch]); const isFilteredAndHidden = loadingState === 'filtered'; let quoteError: React.ReactNode = null; if (isFilteredAndHidden) { quoteError = ( ); } else if (quoteState === 'pending') { quoteError = ( <>

); } else if (quoteState === 'revoked') { quoteError = ( ); } else if ( (quoteState === 'blocked_account' || quoteState === 'blocked_domain' || quoteState === 'muted_account') && !revealed && accountId ) { quoteError = ( ); } else if ( !status || !quotedStatusId || quoteState === 'deleted' || quoteState === 'rejected' || quoteState === 'unauthorized' ) { quoteError = ( ); } else if (hiddenAccount && accountId) { quoteError = ; } if (quoteError) { const hasRemoveButton = contextType === 'composer' && !!onQuoteCancel; return (
{quoteError} {hasRemoveButton && ( )}
); } 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 ( /* @ts-expect-error Status is not yet typed */ ); } /* @ts-expect-error Status is not yet typed */ return ; };