feat(post): add backlinks for quotedCids

This commit is contained in:
plebeius
2026-02-09 19:56:07 +08:00
parent 0fb14e653c
commit c7c8c46557
3 changed files with 64 additions and 12 deletions
+32 -5
View File
@@ -73,6 +73,7 @@ const PostInfo = ({
isPublishing, isPublishing,
onApprove, onApprove,
onReject, onReject,
quotedByMap,
}: PostProps) => { }: PostProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {}; const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
@@ -425,6 +426,16 @@ const PostInfo = ({
reply?.cid && reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />, !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
)} )}
{cid &&
parentCid &&
quotedByMap
?.get(cid)
?.map(
(reply: Comment, index: number) =>
reply?.parentCid !== cid &&
reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
)}
</span> </span>
</div> </div>
); );
@@ -527,7 +538,7 @@ const PostMedia = ({
); );
}; };
const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => { const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
let post = reply; let post = reply;
// handle pending mod or author edit // handle pending mod or author edit
const { editedComment } = useEditedComment({ comment: reply }); const { editedComment } = useEditedComment({ comment: reply });
@@ -555,7 +566,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
<div className={styles.replyDesktop}> <div className={styles.replyDesktop}>
<div className={styles.sideArrows}>{'>>'}</div> <div className={styles.sideArrows}>{'>>'}</div>
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}> <div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
<PostInfo post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} threadNumber={threadNumber} /> <PostInfo post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} threadNumber={threadNumber} quotedByMap={quotedByMap} />
{link && !hidden && !(deleted || removed) && isValidURL(link) && ( {link && !hidden && !(deleted || removed) && isValidURL(link) && (
<PostMedia <PostMedia
commentMediaInfo={commentMediaInfo} commentMediaInfo={commentMediaInfo}
@@ -622,6 +633,22 @@ const PostDesktop = ({
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering // Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]); const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]);
// Compute quotedByMap: map each quoted CID to array of replies that quote it
const quotedByMap = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const quotedCids = reply.quotedCids;
if (quotedCids?.length) {
for (const quotedCid of quotedCids) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
}
}
}
return map;
}, [filteredReplies]);
// Virtuoso scroll position management for infinite replies // Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null); const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = `replies-desktop-${cid}`; const virtuosoStateKey = `replies-desktop-${cid}`;
@@ -747,7 +774,7 @@ const PostDesktop = ({
data={filteredReplies} data={filteredReplies}
itemContent={(index, reply) => ( itemContent={(index, reply) => (
<div className={styles.replyContainer}> <div className={styles.replyContainer}>
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} /> <Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
)} )}
useWindowScroll={true} useWindowScroll={true}
@@ -766,7 +793,7 @@ const PostDesktop = ({
!hasMore && !hasMore &&
filteredReplies.map((reply, index) => ( filteredReplies.map((reply, index) => (
<div key={index} className={styles.replyContainer}> <div key={index} className={styles.replyContainer}>
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} /> <Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
))} ))}
{/* Non-virtualized rendering for board view (last 5 replies or show omitted) */} {/* Non-virtualized rendering for board view (last 5 replies or show omitted) */}
@@ -778,7 +805,7 @@ const PostDesktop = ({
showReplies && showReplies &&
(showOmittedReplies[cid] ? filteredReplies : filteredReplies.slice(-5)).map((reply, index) => ( (showOmittedReplies[cid] ? filteredReplies : filteredReplies.slice(-5)).map((reply, index) => (
<div key={index} className={styles.replyContainer}> <div key={index} className={styles.replyContainer}>
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} /> <Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
))} ))}
</div> </div>
+31 -7
View File
@@ -377,14 +377,14 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
); );
}; };
const ReplyBacklinks = ({ post }: PostProps) => { const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
const { cid, parentCid } = post || {}; const { cid, parentCid } = post || {};
const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } }); const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
return ( return (
cid && cid &&
parentCid && parentCid &&
replies.length > 0 && ( (replies.length > 0 || quotedByMap?.get(cid)?.length) && (
<div className={styles.mobileReplyBacklinks}> <div className={styles.mobileReplyBacklinks}>
{replies.map( {replies.map(
(reply: Comment, index: number) => (reply: Comment, index: number) =>
@@ -392,12 +392,20 @@ const ReplyBacklinks = ({ post }: PostProps) => {
reply?.cid && reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />, !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
)} )}
{quotedByMap
?.get(cid)
?.map(
(reply: Comment, index: number) =>
reply?.parentCid !== cid &&
reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
)}
</div> </div>
) )
); );
}; };
const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => { const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
let post = reply; let post = reply;
// handle pending mod or author edit // handle pending mod or author edit
const { editedComment } = useEditedComment({ comment: reply }); const { editedComment } = useEditedComment({ comment: reply });
@@ -423,7 +431,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
> >
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} /> <PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />} {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
<ReplyBacklinks post={reply} /> <ReplyBacklinks post={reply} quotedByMap={quotedByMap} />
</div> </div>
</div> </div>
</div> </div>
@@ -463,6 +471,22 @@ const PostMobile = ({
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering // Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]); const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]);
// Compute quotedByMap: map each quoted CID to array of replies that quote it
const quotedByMap = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const quotedCids = reply.quotedCids;
if (quotedCids?.length) {
for (const quotedCid of quotedCids) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
}
}
}
return map;
}, [filteredReplies]);
// Virtuoso scroll position management for infinite replies // Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null); const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = `replies-mobile-${cid}`; const virtuosoStateKey = `replies-mobile-${cid}`;
@@ -578,7 +602,7 @@ const PostMobile = ({
data={filteredReplies} data={filteredReplies}
itemContent={(index, reply) => ( itemContent={(index, reply) => (
<div className={styles.replyContainer}> <div className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} /> <Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
)} )}
useWindowScroll={true} useWindowScroll={true}
@@ -597,7 +621,7 @@ const PostMobile = ({
!hasMore && !hasMore &&
filteredReplies.map((reply, index) => ( filteredReplies.map((reply, index) => (
<div key={index} className={styles.replyContainer}> <div key={index} className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} /> <Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
))} ))}
{/* Non-virtualized rendering for board view (last 5 replies) */} {/* Non-virtualized rendering for board view (last 5 replies) */}
@@ -608,7 +632,7 @@ const PostMobile = ({
showReplies && showReplies &&
filteredReplies.slice(-5).map((reply, index) => ( filteredReplies.slice(-5).map((reply, index) => (
<div key={index} className={styles.replyContainer}> <div key={index} className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} /> <Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
))} ))}
</div> </div>
+1
View File
@@ -31,6 +31,7 @@ export interface PostProps {
isPublishing?: boolean; isPublishing?: boolean;
onApprove?: () => void; onApprove?: () => void;
onReject?: () => void; onReject?: () => void;
quotedByMap?: Map<string, Comment[]>;
} }
export const Post = ({ export const Post = ({