mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf: remove reply backlink subscription churn
Replaced per-reply useReplies subscriptions in backlink rendering with a precomputed directRepliesByParentCid map from thread-level filteredReplies. Also optimized quote link rendering in comment-content with scoped store subscription, reduced edit menu state churn, and improved DOM lookups with memoization to keep all backlink rendering data-local and eliminate repeated backend-state-driven reference churn during board scroll.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
||||
@@ -25,6 +25,41 @@ const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
||||
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} isOP={isOP} />;
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
// Subscribe only to CIDs this comment needs so unrelated thread updates do not rerender content.
|
||||
const cidNumbersSignature = usePostNumberStore(
|
||||
useCallback((state) => sortedUniqueCids.map((cid) => `${cid}:${state.cidToNumber[cid] ?? ''}`).join('|'), [sortedUniqueCids]),
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (sortedUniqueCids.length === 0) {
|
||||
return {} as Record<string, number>;
|
||||
}
|
||||
|
||||
const { cidToNumber } = usePostNumberStore.getState();
|
||||
const nextCidToNumber: Record<string, number> = {};
|
||||
|
||||
for (const cid of sortedUniqueCids) {
|
||||
const number = cidToNumber[cid];
|
||||
if (typeof number === 'number') {
|
||||
nextCidToNumber[cid] = number;
|
||||
}
|
||||
}
|
||||
|
||||
return nextCidToNumber;
|
||||
}, [sortedUniqueCids, cidNumbersSignature]);
|
||||
};
|
||||
|
||||
const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
@@ -59,7 +94,15 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
return new Set([...matches].map((m) => parseInt(m[1], 10)));
|
||||
}, [content]);
|
||||
|
||||
const cidToNumber = usePostNumberStore((state) => state.cidToNumber);
|
||||
const relevantQuotedCids = useMemo(() => {
|
||||
const cids = quotedCids ? [...quotedCids] : [];
|
||||
if (parentCid) {
|
||||
cids.push(parentCid);
|
||||
}
|
||||
return cids;
|
||||
}, [quotedCids, parentCid]);
|
||||
|
||||
const cidToNumber = useScopedCidToNumber(relevantQuotedCids);
|
||||
const filteredQuotedCids = useMemo(() => {
|
||||
if (!quotedCids?.length) return [];
|
||||
return quotedCids.filter((cid: string) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { autoUpdate, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
||||
import {
|
||||
@@ -33,30 +33,22 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
const { author, cid, content, deleted, locked, parentCid, pinned, postCid, reason, removed, spoiler, subplebbitAddress } = post || {};
|
||||
const authorDisplayName = post?.author?.displayName;
|
||||
const modBanExpiresAt = post?.commentModeration?.author?.banExpiresAt;
|
||||
const purged = post?.commentModeration?.purged ?? false;
|
||||
const [isEditMenuOpen, setIsEditMenuOpen] = useState(false);
|
||||
const [isContentEditorOpen, setIsContentEditorOpen] = useState(false);
|
||||
|
||||
const account = useAccount();
|
||||
const [signer, setSigner] = useState<any>(account?.signer);
|
||||
|
||||
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useAuthorPrivileges({
|
||||
commentAuthorAddress: author?.address,
|
||||
subplebbitAddress,
|
||||
postCid,
|
||||
});
|
||||
|
||||
const checkSigner = useCallback(() => {
|
||||
if (isAccountCommentAuthor) {
|
||||
setSigner(account?.signer);
|
||||
} else {
|
||||
setSigner(null);
|
||||
}
|
||||
}, [isAccountCommentAuthor, account?.signer]);
|
||||
|
||||
useEffect(() => {
|
||||
checkSigner();
|
||||
}, [checkSigner]);
|
||||
const signer = isAccountCommentAuthor ? account?.signer : null;
|
||||
const latestPostRef = useRef(post);
|
||||
latestPostRef.current = post;
|
||||
const onChallenge = useCallback((...args: any) => addChallenge([...args, latestPostRef.current]), []);
|
||||
|
||||
const defaultPublishEditOptions = useMemo(() => {
|
||||
return {
|
||||
@@ -75,17 +67,17 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
purged: purged ?? false,
|
||||
spoiler: spoiler ?? false,
|
||||
reason,
|
||||
author: post?.commentModeration?.author?.banExpiresAt ? { banExpiresAt: post.commentModeration.author.banExpiresAt } : undefined,
|
||||
author: modBanExpiresAt ? { banExpiresAt: modBanExpiresAt } : undefined,
|
||||
}
|
||||
: undefined,
|
||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
||||
onChallenge,
|
||||
onChallengeVerification: alertChallengeVerificationFailed,
|
||||
onError: (error: Error) => {
|
||||
console.warn(error);
|
||||
alert('Comment edit failed. ' + error.message);
|
||||
},
|
||||
};
|
||||
}, [isAccountMod, isAccountCommentAuthor, cid, content, deleted, locked, pinned, reason, removed, purged, spoiler, subplebbitAddress, post]);
|
||||
}, [isAccountMod, isAccountCommentAuthor, cid, content, deleted, locked, pinned, reason, removed, purged, spoiler, subplebbitAddress, modBanExpiresAt, onChallenge]);
|
||||
|
||||
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState<PublishCommentEditOptions>(defaultPublishEditOptions);
|
||||
|
||||
@@ -94,19 +86,19 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
commentCid: cid,
|
||||
subplebbitAddress,
|
||||
signer,
|
||||
author: signer?.address === author?.address ? { address: signer?.address, displayName: post?.author?.displayName } : account?.author,
|
||||
author: signer?.address === author?.address ? { address: signer?.address, displayName: authorDisplayName } : account?.author,
|
||||
content: publishCommentEditOptions.content,
|
||||
deleted: publishCommentEditOptions.deleted,
|
||||
reason: publishCommentEditOptions.reason,
|
||||
spoiler: publishCommentEditOptions.spoiler,
|
||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
||||
onChallenge,
|
||||
onChallengeVerification: alertChallengeVerificationFailed,
|
||||
onError: (error: Error) => {
|
||||
console.warn(error);
|
||||
alert('Comment edit failed. ' + error.message);
|
||||
},
|
||||
}),
|
||||
[publishCommentEditOptions, cid, subplebbitAddress, signer, post, account?.author, author?.address],
|
||||
[publishCommentEditOptions, cid, subplebbitAddress, signer, account?.author, author?.address, authorDisplayName, onChallenge],
|
||||
);
|
||||
|
||||
const modEditOptions = useMemo<PublishCommentModerationOptions>(
|
||||
@@ -123,27 +115,31 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
author: publishCommentEditOptions.commentModeration?.author,
|
||||
},
|
||||
author: account?.author,
|
||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
||||
onChallenge,
|
||||
onChallengeVerification: alertChallengeVerificationFailed,
|
||||
onError: (error: Error) => {
|
||||
console.warn(error);
|
||||
alert('Comment moderation failed. ' + error.message);
|
||||
},
|
||||
}),
|
||||
[publishCommentEditOptions, cid, subplebbitAddress, post, account?.author, parentCid],
|
||||
[publishCommentEditOptions, cid, subplebbitAddress, account?.author, parentCid, onChallenge],
|
||||
);
|
||||
|
||||
const { publishCommentEdit: publishAuthorEdit } = usePublishCommentEdit(authorEditOptions);
|
||||
const { publishCommentModeration } = usePublishCommentModeration(modEditOptions);
|
||||
|
||||
useEffect(() => {
|
||||
setPublishCommentEditOptions(defaultPublishEditOptions);
|
||||
}, [defaultPublishEditOptions]);
|
||||
|
||||
const [banDuration, setBanDuration] = useState(() =>
|
||||
publishCommentEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(publishCommentEditOptions.commentModeration.author.banExpiresAt) : 1,
|
||||
defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1,
|
||||
);
|
||||
|
||||
const resetMenuState = () => {
|
||||
setPublishCommentEditOptions(defaultPublishEditOptions);
|
||||
setBanDuration(
|
||||
defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1,
|
||||
);
|
||||
setIsContentEditorOpen(false);
|
||||
};
|
||||
|
||||
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { id, checked } = e.target;
|
||||
|
||||
@@ -258,7 +254,12 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
||||
type='checkbox'
|
||||
onChange={() => {
|
||||
if (cid && (isAccountCommentAuthor || isAccountMod)) {
|
||||
setIsEditMenuOpen(!isEditMenuOpen);
|
||||
if (!isEditMenuOpen) {
|
||||
resetMenuState();
|
||||
setIsEditMenuOpen(true);
|
||||
} else {
|
||||
setIsEditMenuOpen(false);
|
||||
}
|
||||
} else {
|
||||
setIsEditMenuOpen(false);
|
||||
alert(parentCid ? t('cannot_edit_reply') : t('cannot_edit_thread'));
|
||||
|
||||
@@ -247,14 +247,18 @@ const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid
|
||||
};
|
||||
|
||||
const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
||||
const remarkPlugins: any[] = [[supersub]];
|
||||
const remarkPlugins = useMemo(() => {
|
||||
const plugins: any[] = [[supersub]];
|
||||
|
||||
if (content && content.length <= MAX_LENGTH_FOR_GFM) {
|
||||
remarkPlugins.push([remarkGfm, { singleTilde: false }]);
|
||||
plugins.push([remarkGfm, { singleTilde: false }]);
|
||||
}
|
||||
|
||||
remarkPlugins.push([blockquoteToGreentext]);
|
||||
remarkPlugins.push([spoilerTransform]);
|
||||
plugins.push([blockquoteToGreentext]);
|
||||
plugins.push([spoilerTransform]);
|
||||
|
||||
return plugins;
|
||||
}, [content]);
|
||||
|
||||
const customSchema = useMemo(
|
||||
() => ({
|
||||
@@ -270,25 +274,18 @@ const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
||||
[],
|
||||
);
|
||||
|
||||
const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||
|
||||
const rehypePlugins = useMemo(() => [[rehypeRaw as any], [rehypeSanitize, customSchema]] as any[], [customSchema]);
|
||||
|
||||
// Preprocess content to convert plain text 5chan patterns to markdown links
|
||||
const processedContent = preprocess5chanPatterns(content || '');
|
||||
const processedContent = useMemo(() => preprocess5chanPatterns(content || ''), [content]);
|
||||
|
||||
return (
|
||||
<span className={styles.markdown}>
|
||||
{isInCatalogView && title && (
|
||||
<span>
|
||||
<b>{title}</b>
|
||||
{content ? ': ' : ''}
|
||||
</span>
|
||||
)}
|
||||
<ReactMarkdown
|
||||
children={processedContent}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={[[rehypeRaw as any], [rehypeSanitize, customSchema]]}
|
||||
components={
|
||||
{
|
||||
const components = useMemo(
|
||||
() =>
|
||||
({
|
||||
p: ({ children }) => <p className={isInCatalogView ? styles.inline : ''}>{children}</p>,
|
||||
h1: ({ children }) => <p className={styles.header}>{children}</p>,
|
||||
h2: ({ children }) => <p className={styles.header}>{children}</p>,
|
||||
@@ -321,9 +318,19 @@ const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
||||
|
||||
return renderAnchorLink(children, href || '', postCid);
|
||||
},
|
||||
} as ExtendedComponents
|
||||
}
|
||||
/>
|
||||
}) as ExtendedComponents,
|
||||
[isInCatalogView, postCid],
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={styles.markdown}>
|
||||
{isInCatalogView && title && (
|
||||
<span>
|
||||
<b>{title}</b>
|
||||
{content ? ': ' : ''}
|
||||
</span>
|
||||
)}
|
||||
<ReactMarkdown children={processedContent} remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={components} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,11 +78,11 @@ const PostInfo = ({
|
||||
onApprove,
|
||||
onReject,
|
||||
quotedByMap,
|
||||
}: PostProps) => {
|
||||
directRepliesByParentCid,
|
||||
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
||||
const title = post?.title?.trim();
|
||||
const { replies } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
|
||||
const { address, shortAddress } = author || {};
|
||||
const displayName = author?.displayName?.trim();
|
||||
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
||||
@@ -205,12 +205,18 @@ const PostInfo = ({
|
||||
const userIDBackgroundColor = hashStringToColor(userID);
|
||||
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||
|
||||
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||
const showUserID = pseudonymityMode !== 'per-reply';
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
const numberOfPostsByAuthor = useMemo(() => {
|
||||
if (!showUserID || deleted || removed || !shortAddress || !postCid || typeof document === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||
}, [showUserID, deleted, removed, shortAddress, postCid]);
|
||||
|
||||
const { hidden } = useHide(post);
|
||||
|
||||
const { openReplyModal } = useReplyModalStore();
|
||||
@@ -421,18 +427,35 @@ const PostInfo = ({
|
||||
)}
|
||||
</span>
|
||||
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
||||
{cid &&
|
||||
parentCid &&
|
||||
replies &&
|
||||
replies.map(
|
||||
{cid && parentCid && <ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
||||
{cid && !parentCid && <OpBacklinks cid={cid} quotedByMap={quotedByMap} />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReplyBacklinks = ({
|
||||
post,
|
||||
quotedByMap,
|
||||
directRepliesByParentCid,
|
||||
}: {
|
||||
post: Comment;
|
||||
quotedByMap?: Map<string, Comment[]>;
|
||||
directRepliesByParentCid?: Map<string, Comment[]>;
|
||||
}) => {
|
||||
const { cid, parentCid } = post || {};
|
||||
if (!cid || !parentCid) {
|
||||
return null;
|
||||
}
|
||||
const directReplies = directRepliesByParentCid?.get(cid) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{directReplies.map(
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{cid &&
|
||||
parentCid &&
|
||||
quotedByMap
|
||||
{quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment, index: number) =>
|
||||
@@ -440,18 +463,20 @@ const PostInfo = ({
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{cid &&
|
||||
!parentCid &&
|
||||
quotedByMap
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OpBacklinks = ({ cid, quotedByMap }: { cid: string; quotedByMap?: Map<string, Comment[]> }) => (
|
||||
<>
|
||||
{quotedByMap
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment) =>
|
||||
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface PostMediaProps {
|
||||
commentMediaInfo: CommentMediaInfo | undefined;
|
||||
@@ -550,7 +575,14 @@ const PostMedia = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
||||
const Reply = ({
|
||||
postReplyCount,
|
||||
reply,
|
||||
roles,
|
||||
threadNumber,
|
||||
quotedByMap,
|
||||
directRepliesByParentCid,
|
||||
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
||||
let post = reply;
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: reply });
|
||||
@@ -578,7 +610,15 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: Post
|
||||
<div className={styles.replyDesktop}>
|
||||
<div className={styles.sideArrows}>{'>>'}</div>
|
||||
<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} quotedByMap={quotedByMap} />
|
||||
<PostInfo
|
||||
post={post}
|
||||
postReplyCount={postReplyCount}
|
||||
roles={roles}
|
||||
isHidden={hidden}
|
||||
threadNumber={threadNumber}
|
||||
quotedByMap={quotedByMap}
|
||||
directRepliesByParentCid={directRepliesByParentCid}
|
||||
/>
|
||||
{link && !hidden && !(deleted || removed) && isValidURL(link) && (
|
||||
<PostMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
@@ -668,6 +708,22 @@ const PostDesktop = ({
|
||||
|
||||
// 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 directRepliesByParentCid = useMemo(() => {
|
||||
const map = new Map<string, Comment[]>();
|
||||
for (const reply of filteredReplies) {
|
||||
const directParentCid = reply?.parentCid;
|
||||
if (!directParentCid || !reply?.cid) {
|
||||
continue;
|
||||
}
|
||||
const existingReplies = map.get(directParentCid);
|
||||
if (existingReplies) {
|
||||
existingReplies.push(reply);
|
||||
} else {
|
||||
map.set(directParentCid, [reply]);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [filteredReplies]);
|
||||
|
||||
const quotedByMap = useQuotedByMap(filteredReplies);
|
||||
|
||||
@@ -760,6 +816,7 @@ const PostDesktop = ({
|
||||
onApprove={onApprove}
|
||||
onReject={onReject}
|
||||
quotedByMap={quotedByMap}
|
||||
directRepliesByParentCid={directRepliesByParentCid}
|
||||
/>
|
||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||
{!isHidden && <CommentContent comment={post} />}
|
||||
@@ -797,7 +854,14 @@ const PostDesktop = ({
|
||||
data={filteredReplies}
|
||||
itemContent={(index, reply) => (
|
||||
<div className={styles.replyContainer}>
|
||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
|
||||
<Reply
|
||||
reply={reply}
|
||||
roles={roles}
|
||||
postReplyCount={replyCount}
|
||||
threadNumber={post?.number}
|
||||
quotedByMap={quotedByMap}
|
||||
directRepliesByParentCid={directRepliesByParentCid}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
useWindowScroll={true}
|
||||
@@ -816,7 +880,14 @@ const PostDesktop = ({
|
||||
!hasMore &&
|
||||
filteredReplies.map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
|
||||
<Reply
|
||||
reply={reply}
|
||||
roles={roles}
|
||||
postReplyCount={replyCount}
|
||||
threadNumber={post?.number}
|
||||
quotedByMap={quotedByMap}
|
||||
directRepliesByParentCid={directRepliesByParentCid}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{/* Non-virtualized rendering for board view (last 5 replies or show omitted) */}
|
||||
@@ -828,7 +899,14 @@ const PostDesktop = ({
|
||||
showReplies &&
|
||||
(showOmittedReplies[cid] ? filteredReplies : filteredReplies.slice(-5)).map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
|
||||
<Reply
|
||||
reply={reply}
|
||||
roles={roles}
|
||||
postReplyCount={replyCount}
|
||||
threadNumber={post?.number}
|
||||
quotedByMap={quotedByMap}
|
||||
directRepliesByParentCid={directRepliesByParentCid}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -171,12 +171,18 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
const stateString = useStateString(post);
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||
|
||||
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||
const showUserID = pseudonymityMode !== 'per-reply';
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
const numberOfPostsByAuthor = useMemo(() => {
|
||||
if (!showUserID || deleted || removed || !shortAddress || !postCid || typeof document === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||
}, [showUserID, deleted, removed, shortAddress, postCid]);
|
||||
|
||||
const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
|
||||
const userIDBackgroundColor = hashStringToColor(userID);
|
||||
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { QUOTE_NUMBER_REGEX } from '../lib/utils/url-utils';
|
||||
import usePostNumberStore from '../stores/use-post-number-store';
|
||||
@@ -8,6 +8,30 @@ interface ReplyQuoteTargets {
|
||||
quotedPostNumbers: number[];
|
||||
}
|
||||
|
||||
const getReplyFingerprint = (reply: Comment) =>
|
||||
`${reply?.cid ?? ''}|${reply?.deleted ? '1' : '0'}|${reply?.removed ? '1' : '0'}|${reply?.edit?.timestamp ?? ''}|${reply?.state ?? ''}`;
|
||||
|
||||
const areQuotedByMapsEquivalent = (previousMap: Map<string, Comment[]>, nextMap: Map<string, Comment[]>) => {
|
||||
if (previousMap.size !== nextMap.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [quotedCid, nextReplies] of nextMap) {
|
||||
const previousReplies = previousMap.get(quotedCid);
|
||||
if (!previousReplies || previousReplies.length !== nextReplies.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < nextReplies.length; i++) {
|
||||
if (getReplyFingerprint(previousReplies[i]) !== getReplyFingerprint(nextReplies[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const extractReplyQuoteTargets = (replies: Comment[]) => {
|
||||
const quotedPostNumbers = new Set<number>();
|
||||
const replyQuoteTargets: ReplyQuoteTargets[] = [];
|
||||
@@ -38,6 +62,7 @@ const extractReplyQuoteTargets = (replies: Comment[]) => {
|
||||
};
|
||||
|
||||
const useQuotedByMap = (replies: Comment[] = []) => {
|
||||
const stableQuotedByMapRef = useRef<Map<string, Comment[]>>(new Map());
|
||||
const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]);
|
||||
|
||||
// Subscribe only to post numbers referenced in this thread to avoid unrelated global store churn.
|
||||
@@ -92,6 +117,11 @@ const useQuotedByMap = (replies: Comment[] = []) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (areQuotedByMapsEquivalent(stableQuotedByMapRef.current, map)) {
|
||||
return stableQuotedByMapRef.current;
|
||||
}
|
||||
|
||||
stableQuotedByMapRef.current = map;
|
||||
return map;
|
||||
}, [replyQuoteTargets, quotedNumberToCid]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user