mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-05 09:21:11 +00:00

Some checks are pending
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
236 lines
6.5 KiB
TypeScript
236 lines
6.5 KiB
TypeScript
import { useEffect, useMemo } from 'react';
|
|
|
|
import { FormattedMessage } from 'react-intl';
|
|
|
|
import classNames from 'classnames';
|
|
|
|
import type { Map as ImmutableMap } from 'immutable';
|
|
|
|
import { LearnMoreLink } from 'mastodon/components/learn_more_link';
|
|
import StatusContainer from 'mastodon/containers/status_container';
|
|
import type { Status } from 'mastodon/models/status';
|
|
import type { RootState } from 'mastodon/store';
|
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
|
|
|
import { fetchStatus } from '../actions/statuses';
|
|
import { makeGetStatus } from '../selectors';
|
|
|
|
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
|
|
|
const QuoteWrapper: React.FC<{
|
|
isError?: boolean;
|
|
children: React.ReactElement;
|
|
}> = ({ isError, children }) => {
|
|
return (
|
|
<div
|
|
className={classNames('status__quote', {
|
|
'status__quote--error': isError,
|
|
})}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const NestedQuoteLink: 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?.acct;
|
|
|
|
if (!quoteAuthorName) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className='status__quote-author-button'>
|
|
<FormattedMessage
|
|
id='status.quote_post_author'
|
|
defaultMessage='Quoted a post by @{name}'
|
|
values={{ name: quoteAuthorName }}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
|
|
type GetStatusSelector = (
|
|
state: RootState,
|
|
props: { id?: string | null; contextType?: string },
|
|
) => Status | null;
|
|
|
|
interface QuotedStatusProps {
|
|
quote: QuoteMap;
|
|
contextType?: string;
|
|
parentQuotePostId?: string | null;
|
|
variant?: 'full' | 'link';
|
|
nestingLevel?: number;
|
|
onQuoteCancel?: () => void; // Used for composer.
|
|
}
|
|
|
|
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
|
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 status = useAppSelector((state) =>
|
|
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
|
);
|
|
|
|
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
|
|
|
|
useEffect(() => {
|
|
if (shouldLoadQuote && quotedStatusId) {
|
|
dispatch(
|
|
fetchStatus(quotedStatusId, {
|
|
parentQuotePostId,
|
|
alsoFetchContext: false,
|
|
}),
|
|
);
|
|
}
|
|
}, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]);
|
|
|
|
// 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;
|
|
|
|
let quoteError: React.ReactNode = null;
|
|
|
|
if (isFilteredAndHidden) {
|
|
quoteError = (
|
|
<FormattedMessage
|
|
id='status.quote_error.filtered'
|
|
defaultMessage='Hidden due to one of your filters'
|
|
/>
|
|
);
|
|
} else if (quoteState === 'pending') {
|
|
quoteError = (
|
|
<>
|
|
<FormattedMessage
|
|
id='status.quote_error.pending_approval'
|
|
defaultMessage='Post pending'
|
|
/>
|
|
|
|
<LearnMoreLink>
|
|
<h6>
|
|
<FormattedMessage
|
|
id='status.quote_error.pending_approval_popout.title'
|
|
defaultMessage='Pending quote? Remain calm'
|
|
/>
|
|
</h6>
|
|
<p>
|
|
<FormattedMessage
|
|
id='status.quote_error.pending_approval_popout.body'
|
|
defaultMessage='Quotes shared across the Fediverse may take time to display, as different servers have different protocols.'
|
|
/>
|
|
</p>
|
|
</LearnMoreLink>
|
|
</>
|
|
);
|
|
} else if (
|
|
!status ||
|
|
!quotedStatusId ||
|
|
quoteState === 'deleted' ||
|
|
quoteState === 'rejected' ||
|
|
quoteState === 'revoked' ||
|
|
quoteState === 'unauthorized'
|
|
) {
|
|
quoteError = (
|
|
<FormattedMessage
|
|
id='status.quote_error.not_available'
|
|
defaultMessage='Post unavailable'
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (quoteError) {
|
|
return <QuoteWrapper isError>{quoteError}</QuoteWrapper>;
|
|
}
|
|
|
|
if (variant === 'link' && status) {
|
|
return <NestedQuoteLink status={status} />;
|
|
}
|
|
|
|
const childQuote = status?.get('quote') as QuoteMap | undefined;
|
|
const canRenderChildQuote =
|
|
childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL;
|
|
|
|
return (
|
|
<QuoteWrapper>
|
|
{/* @ts-expect-error Status is not yet typed */}
|
|
<StatusContainer
|
|
isQuotedPost
|
|
id={quotedStatusId}
|
|
contextType={contextType}
|
|
avatarSize={32}
|
|
onQuoteCancel={onQuoteCancel}
|
|
>
|
|
{canRenderChildQuote && (
|
|
<QuotedStatus
|
|
quote={childQuote}
|
|
parentQuotePostId={quotedStatusId}
|
|
contextType={contextType}
|
|
variant={
|
|
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
|
|
}
|
|
nestingLevel={nestingLevel + 1}
|
|
/>
|
|
)}
|
|
</StatusContainer>
|
|
</QuoteWrapper>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<StatusContainer {...props}>
|
|
<QuotedStatus
|
|
quote={quote}
|
|
parentQuotePostId={status?.get('id') as string}
|
|
contextType={props.contextType}
|
|
/>
|
|
</StatusContainer>
|
|
);
|
|
}
|
|
|
|
return <StatusContainer {...props} />;
|
|
};
|