2026-02-24 15:15:44 +08:00
|
|
|
import { Fragment, useMemo, useState } from 'react';
|
2025-02-22 22:22:42 +01:00
|
|
|
import { useLocation, useParams } from 'react-router-dom';
|
|
|
|
|
import { Trans, useTranslation } from 'react-i18next';
|
2026-01-19 14:34:26 +01:00
|
|
|
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
2025-03-05 21:38:01 +01:00
|
|
|
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
|
2026-02-10 16:23:15 +08:00
|
|
|
import usePostNumberStore from '../../stores/use-post-number-store';
|
2026-01-09 16:28:17 +01:00
|
|
|
import Plebbit from '@plebbit/plebbit-js';
|
2025-02-22 22:22:42 +01:00
|
|
|
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
|
|
|
|
import { isPostPageView } from '../../lib/utils/view-utils';
|
|
|
|
|
import useIsMobile from '../../hooks/use-is-mobile';
|
|
|
|
|
import useStateString from '../../hooks/use-state-string';
|
|
|
|
|
import LoadingEllipsis from '../../components/loading-ellipsis';
|
|
|
|
|
import ReplyQuotePreview from '../../components/reply-quote-preview';
|
|
|
|
|
import Markdown from '../../components/markdown';
|
|
|
|
|
import Tooltip from '../../components/tooltip';
|
|
|
|
|
import styles from '../../views/post/post.module.css';
|
2026-02-18 15:31:01 +08:00
|
|
|
import capitalize from 'lodash/capitalize';
|
2025-02-22 22:22:42 +01:00
|
|
|
|
2026-02-09 19:23:15 +08:00
|
|
|
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
|
|
|
|
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
|
|
|
|
|
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
|
|
|
|
|
// Prefer hook version to ensure 'number' property is populated for deeper nested replies in Virtuoso
|
|
|
|
|
const quotedComment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
|
|
|
|
|
const isOP = cid === postCid;
|
|
|
|
|
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} isOP={isOP} />;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 20:00:32 +08:00
|
|
|
const useScopedCidToNumber = (cids: string[]) => {
|
|
|
|
|
const sortedUniqueCids = useMemo(() => {
|
|
|
|
|
const uniqueCids = new Set<string>();
|
|
|
|
|
for (const cid of cids) {
|
|
|
|
|
if (cid) {
|
|
|
|
|
uniqueCids.add(cid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [...uniqueCids].sort();
|
|
|
|
|
}, [cids]);
|
|
|
|
|
|
2026-02-24 15:15:44 +08:00
|
|
|
const cidToNumber = usePostNumberStore((s) => s.cidToNumber);
|
2026-02-12 20:00:32 +08:00
|
|
|
|
2026-02-24 15:15:44 +08:00
|
|
|
if (sortedUniqueCids.length === 0) {
|
|
|
|
|
return {} as Record<string, number>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextCidToNumber: Record<string, number> = {};
|
|
|
|
|
for (const cid of sortedUniqueCids) {
|
|
|
|
|
const number = cidToNumber[cid];
|
|
|
|
|
if (typeof number === 'number') {
|
|
|
|
|
nextCidToNumber[cid] = number;
|
2026-02-12 20:00:32 +08:00
|
|
|
}
|
2026-02-24 15:15:44 +08:00
|
|
|
}
|
|
|
|
|
return nextCidToNumber;
|
2026-02-12 20:00:32 +08:00
|
|
|
};
|
|
|
|
|
|
2025-03-05 21:38:01 +01:00
|
|
|
const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
2025-02-22 22:22:42 +01:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const isInPostView = isPostPageView(location.pathname, params);
|
|
|
|
|
const [showOriginal, setShowOriginal] = useState(false);
|
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
|
|
2026-02-09 19:23:15 +08:00
|
|
|
const { cid, content, deleted, edit, original, parentCid, postCid, pendingApproval, quotedCids, reason, removed, state, subplebbitAddress } = post || {};
|
2026-01-09 16:28:17 +01:00
|
|
|
const banned = !!post?.author?.subplebbit?.banExpiresAt;
|
2025-02-22 22:22:42 +01:00
|
|
|
|
|
|
|
|
const [showFullComment, setShowFullComment] = useState(false);
|
|
|
|
|
const displayContent =
|
|
|
|
|
content &&
|
|
|
|
|
(!isInPostView && content.length > 1000 && !showFullComment
|
|
|
|
|
? content.slice(0, 1000)
|
|
|
|
|
: isInPostView && content.length > 2000 && !showFullComment
|
2026-01-08 17:09:35 +01:00
|
|
|
? content.slice(0, 2000)
|
|
|
|
|
: content);
|
2025-02-22 22:22:42 +01:00
|
|
|
|
2026-01-19 14:34:26 +01:00
|
|
|
const quotelinkReplyFromStore = useSubplebbitsPagesStore((state) => state.comments[parentCid]);
|
|
|
|
|
const quotelinkReplyFromHook = useComment({ commentCid: parentCid, onlyIfCached: true });
|
|
|
|
|
// Prefer hook version to ensure 'number' property is populated for deeper nested replies in Virtuoso
|
|
|
|
|
const quotelinkReply = quotelinkReplyFromHook?.number !== undefined ? quotelinkReplyFromHook : quotelinkReplyFromStore;
|
2025-03-05 17:10:26 +01:00
|
|
|
|
|
|
|
|
const isReply = !!parentCid;
|
|
|
|
|
const isReplyingToReply = isReply && parentCid !== postCid;
|
2025-02-22 22:22:42 +01:00
|
|
|
|
2026-02-24 15:15:44 +08:00
|
|
|
const contentNumbers = !content ? new Set<number>() : new Set([...content.matchAll(/(?<![>/\w])>>(\d+)(?![\d/])/g)].map((m) => parseInt(m[1], 10)));
|
2026-02-10 16:23:15 +08:00
|
|
|
|
2026-02-24 15:15:44 +08:00
|
|
|
const relevantQuotedCids = (() => {
|
2026-02-12 20:00:32 +08:00
|
|
|
const cids = quotedCids ? [...quotedCids] : [];
|
|
|
|
|
if (parentCid) {
|
|
|
|
|
cids.push(parentCid);
|
|
|
|
|
}
|
|
|
|
|
return cids;
|
2026-02-24 15:15:44 +08:00
|
|
|
})();
|
2026-02-12 20:00:32 +08:00
|
|
|
|
|
|
|
|
const cidToNumber = useScopedCidToNumber(relevantQuotedCids);
|
2026-02-24 15:15:44 +08:00
|
|
|
const filteredQuotedCids = !quotedCids?.length
|
|
|
|
|
? []
|
|
|
|
|
: quotedCids.filter((cid: string) => {
|
|
|
|
|
const num = cidToNumber[cid];
|
|
|
|
|
return num === undefined || !contentNumbers.has(num);
|
|
|
|
|
});
|
2026-02-10 16:23:15 +08:00
|
|
|
|
|
|
|
|
const shouldShowReplyingToReply = isReplyingToReply && (parentCid ? !contentNumbers.has(cidToNumber[parentCid] ?? -1) : true);
|
|
|
|
|
|
2025-02-22 22:22:42 +01:00
|
|
|
const stateString = useStateString(post);
|
2026-02-13 17:48:27 +08:00
|
|
|
const hasFailedState = state === 'failed';
|
2025-02-22 22:22:42 +01:00
|
|
|
|
2026-02-13 17:48:27 +08:00
|
|
|
const loadingString = (
|
|
|
|
|
<div className={styles.stateString}>{!hasFailedState ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString || capitalize(t('failed'))}</div>
|
|
|
|
|
);
|
2025-02-22 22:22:42 +01:00
|
|
|
|
|
|
|
|
return (
|
2025-12-23 19:14:25 +01:00
|
|
|
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
|
2026-02-09 19:23:15 +08:00
|
|
|
{isReply &&
|
2026-02-13 17:48:27 +08:00
|
|
|
!hasFailedState &&
|
2026-02-09 19:23:15 +08:00
|
|
|
!(deleted || removed) &&
|
2026-02-10 16:23:15 +08:00
|
|
|
(filteredQuotedCids.length > 0
|
|
|
|
|
? filteredQuotedCids.map((cid: string) => <QuotedCidLink key={cid} cid={cid} postCid={postCid} />)
|
|
|
|
|
: shouldShowReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />)}
|
2025-02-22 22:22:42 +01:00
|
|
|
{removed ? (
|
|
|
|
|
reason ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className={styles.redEditMessage}>({t('this_post_was_removed')})</span>
|
|
|
|
|
<br />
|
|
|
|
|
<br />
|
2026-02-11 19:01:09 +08:00
|
|
|
<span className={styles.grayEditMessage}>{`${capitalize(t('reason'))}: "${reason}"`}</span>
|
2025-02-22 22:22:42 +01:00
|
|
|
</>
|
|
|
|
|
) : (
|
2026-02-11 19:01:09 +08:00
|
|
|
<span className={styles.grayEditMessage}>{capitalize(t('this_post_was_removed'))}.</span>
|
2025-02-22 22:22:42 +01:00
|
|
|
)
|
|
|
|
|
) : deleted ? (
|
|
|
|
|
reason ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>{' '}
|
2026-02-11 19:01:09 +08:00
|
|
|
<span className={styles.grayEditMessage}>{`${capitalize(t('reason'))}: "${reason}"`}</span>
|
2025-02-22 22:22:42 +01:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>
|
|
|
|
|
)
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2026-02-10 16:23:15 +08:00
|
|
|
{!showOriginal && <Markdown content={displayContent} postCid={postCid} />}
|
2026-01-08 17:09:35 +01:00
|
|
|
{pendingApproval && (
|
|
|
|
|
<>
|
|
|
|
|
<br />
|
2026-01-26 16:31:15 +08:00
|
|
|
<span className={styles.pendingApproval}>({t('pending_mod_approval')})</span>
|
2026-01-08 17:09:35 +01:00
|
|
|
</>
|
|
|
|
|
)}
|
2025-02-22 22:22:42 +01:00
|
|
|
{((!isInPostView && content?.length > 1000 && !showFullComment) || (isInPostView && content?.length > 2000 && !showFullComment)) && (
|
|
|
|
|
<span className={styles.abbr}>
|
|
|
|
|
<br />
|
|
|
|
|
<br />
|
2026-02-24 15:15:44 +08:00
|
|
|
<Trans
|
|
|
|
|
i18nKey={'comment_too_long'}
|
|
|
|
|
shouldUnescape={true}
|
|
|
|
|
components={{
|
|
|
|
|
1: (
|
|
|
|
|
<span
|
|
|
|
|
key={cid}
|
|
|
|
|
role='button'
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setShowFullComment(true);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => setShowFullComment(true)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-02-22 22:22:42 +01:00
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{edit && original?.content !== content && (
|
|
|
|
|
<span className={styles.editedInfo}>
|
2026-02-10 16:23:15 +08:00
|
|
|
{showOriginal && <Markdown content={original?.content} postCid={postCid} />}
|
2025-02-22 22:22:42 +01:00
|
|
|
<br />
|
|
|
|
|
<br />
|
|
|
|
|
<Trans
|
|
|
|
|
i18nKey={'comment_edited_at_timestamp'}
|
|
|
|
|
values={{ timestamp: getFormattedDate(edit?.timestamp) }}
|
|
|
|
|
shouldUnescape={true}
|
2025-03-05 18:30:41 +01:00
|
|
|
components={{
|
2026-02-24 15:15:44 +08:00
|
|
|
1: (
|
|
|
|
|
<Tooltip key={edit?.timestamp} content={getFormattedTimeAgo(edit?.timestamp)}>
|
|
|
|
|
<Fragment key={edit?.timestamp}></Fragment>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
),
|
2025-03-05 18:30:41 +01:00
|
|
|
}}
|
2025-02-22 22:22:42 +01:00
|
|
|
/>{' '}
|
|
|
|
|
{reason && <>{t('reason_reason', { reason: reason, interpolation: { escapeValue: false } })} </>}
|
|
|
|
|
{showOriginal ? (
|
|
|
|
|
<Trans
|
|
|
|
|
i18nKey={'click_here_to_hide_original'}
|
|
|
|
|
shouldUnescape={true}
|
2026-02-24 15:15:44 +08:00
|
|
|
components={{
|
|
|
|
|
1: (
|
|
|
|
|
<span
|
|
|
|
|
key={cid}
|
|
|
|
|
className={styles.showOriginal}
|
|
|
|
|
role='button'
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setShowOriginal(!showOriginal);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => setShowOriginal(!showOriginal)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
}}
|
2025-02-22 22:22:42 +01:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Trans
|
|
|
|
|
i18nKey={'click_here_to_show_original'}
|
|
|
|
|
shouldUnescape={true}
|
2026-02-24 15:15:44 +08:00
|
|
|
components={{
|
|
|
|
|
1: (
|
|
|
|
|
<span
|
|
|
|
|
key={cid}
|
|
|
|
|
className={styles.showOriginal}
|
|
|
|
|
role='button'
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setShowOriginal(!showOriginal);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => setShowOriginal(!showOriginal)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
}}
|
2025-02-22 22:22:42 +01:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-01-09 16:28:17 +01:00
|
|
|
{banned && (
|
2025-02-22 22:22:42 +01:00
|
|
|
<span className={styles.removedContent}>
|
|
|
|
|
<br />
|
|
|
|
|
<br />
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={`${t('ban_expires_at', {
|
2025-12-24 13:29:09 +01:00
|
|
|
address: subplebbitAddress && Plebbit.getShortAddress({ address: subplebbitAddress }),
|
2026-01-09 16:28:17 +01:00
|
|
|
timestamp: getFormattedDate(post?.author?.subplebbit?.banExpiresAt),
|
2025-02-22 22:22:42 +01:00
|
|
|
interpolation: { escapeValue: false },
|
2026-02-11 19:01:09 +08:00
|
|
|
})}${reason ? `. ${capitalize(t('reason'))}: "${reason}"` : ''}`}
|
2026-02-24 15:15:44 +08:00
|
|
|
>
|
|
|
|
|
{`(${t('user_banned')})`}
|
|
|
|
|
</Tooltip>
|
2025-02-22 22:22:42 +01:00
|
|
|
</span>
|
2026-01-09 16:28:17 +01:00
|
|
|
)}
|
2026-02-13 17:48:27 +08:00
|
|
|
{!cid && !hasFailedState && (
|
2025-02-22 22:22:42 +01:00
|
|
|
<>
|
|
|
|
|
<br />
|
|
|
|
|
<br />
|
|
|
|
|
{loadingString}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</blockquote>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default CommentContent;
|