diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index e363a35f..bda32a47 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -8,7 +8,7 @@ import styles from '../../views/post/post.module.css'; import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils'; import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; -import { isValidURL, QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils'; +import { isValidURL } from '../../lib/utils/url-utils'; import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { formatUserIDForDisplay } from '../../lib/utils/string-utils'; import useModQueueStore from '../../stores/use-mod-queue-store'; @@ -42,6 +42,7 @@ import useFeedResetStore from '../../stores/use-feed-reset-store'; import usePostNumberStore from '../../stores/use-post-number-store'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks'; +import useQuotedByMap from '../../hooks/use-quoted-by-map'; const { addChallenge } = useChallengesStore.getState(); @@ -639,7 +640,6 @@ const PostDesktop = ({ } }, [isInPostPageView, isInPendingPostView, reset, setResetFunction]); const registerComments = usePostNumberStore((s) => s.registerComments); - const numberToCid = usePostNumberStore((s) => s.numberToCid); const prevCidsRef = useRef(''); useEffect(() => { const all = post ? [post, ...(replies || [])] : replies || []; @@ -669,36 +669,7 @@ 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]); - // Compute quotedByMap: map each quoted CID to array of replies that quote it - const quotedByMap = useMemo(() => { - const map = new Map(); - for (const reply of filteredReplies) { - const cidSet = new Set(); - - if (reply.quotedCids?.length) { - for (const quotedCid of reply.quotedCids) { - cidSet.add(quotedCid); - } - } - - if (reply.content) { - for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) { - const postNumber = parseInt(match[1], 10); - const quotedCid = numberToCid[postNumber]; - if (quotedCid) { - cidSet.add(quotedCid); - } - } - } - - for (const quotedCid of cidSet) { - const arr = map.get(quotedCid); - if (arr) arr.push(reply); - else map.set(quotedCid, [reply]); - } - } - return map; - }, [filteredReplies, numberToCid]); + const quotedByMap = useQuotedByMap(filteredReplies); // Virtuoso scroll position management for infinite replies const virtuosoRef = useRef(null); diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 2b695cf9..f00c09ab 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -9,7 +9,6 @@ import { shouldShowSnow } from '../../lib/snow'; import { getHasThumbnail } from '../../lib/utils/media-utils'; import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; -import { QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils'; import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { formatUserIDForDisplay } from '../../lib/utils/string-utils'; import useModQueueStore from '../../stores/use-mod-queue-store'; @@ -37,6 +36,7 @@ import useChallengesStore from '../../stores/use-challenges-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import usePostNumberStore from '../../stores/use-post-number-store'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; +import useQuotedByMap from '../../hooks/use-quoted-by-map'; const { addChallenge } = useChallengesStore.getState(); @@ -489,7 +489,6 @@ const PostMobile = ({ } }, [isInPostView, isInPendingPostView, reset, setResetFunction]); const registerComments = usePostNumberStore((s) => s.registerComments); - const numberToCid = usePostNumberStore((s) => s.numberToCid); const prevCidsRef = useRef(''); useEffect(() => { const all = post ? [post, ...(replies || [])] : replies || []; @@ -512,36 +511,7 @@ const PostMobile = ({ // 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]); - // Compute quotedByMap: map each quoted CID to array of replies that quote it - const quotedByMap = useMemo(() => { - const map = new Map(); - for (const reply of filteredReplies) { - const cidSet = new Set(); - - if (reply.quotedCids?.length) { - for (const quotedCid of reply.quotedCids) { - cidSet.add(quotedCid); - } - } - - if (reply.content) { - for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) { - const postNumber = parseInt(match[1], 10); - const quotedCid = numberToCid[postNumber]; - if (quotedCid) { - cidSet.add(quotedCid); - } - } - } - - for (const quotedCid of cidSet) { - const arr = map.get(quotedCid); - if (arr) arr.push(reply); - else map.set(quotedCid, [reply]); - } - } - return map; - }, [filteredReplies, numberToCid]); + const quotedByMap = useQuotedByMap(filteredReplies); // Virtuoso scroll position management for infinite replies const virtuosoRef = useRef(null); diff --git a/src/hooks/use-quoted-by-map.ts b/src/hooks/use-quoted-by-map.ts new file mode 100644 index 00000000..9e69c1b1 --- /dev/null +++ b/src/hooks/use-quoted-by-map.ts @@ -0,0 +1,99 @@ +import { useCallback, useMemo } 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'; + +interface ReplyQuoteTargets { + reply: Comment; + quotedPostNumbers: number[]; +} + +const extractReplyQuoteTargets = (replies: Comment[]) => { + const quotedPostNumbers = new Set(); + const replyQuoteTargets: ReplyQuoteTargets[] = []; + + for (const reply of replies) { + const quotedPostNumbersForReply: number[] = []; + + if (reply.content) { + for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) { + const postNumber = Number.parseInt(match[1], 10); + if (!Number.isNaN(postNumber)) { + quotedPostNumbersForReply.push(postNumber); + quotedPostNumbers.add(postNumber); + } + } + } + + replyQuoteTargets.push({ + reply, + quotedPostNumbers: quotedPostNumbersForReply, + }); + } + + return { + replyQuoteTargets, + quotedPostNumbers: [...quotedPostNumbers].sort((a, b) => a - b), + }; +}; + +const useQuotedByMap = (replies: Comment[] = []) => { + const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]); + + // Subscribe only to post numbers referenced in this thread to avoid unrelated global store churn. + const quotedNumbersSignature = usePostNumberStore( + useCallback((state) => quotedPostNumbers.map((postNumber) => `${postNumber}:${state.numberToCid[postNumber] ?? ''}`).join('|'), [quotedPostNumbers]), + ); + + const quotedNumberToCid = useMemo(() => { + if (quotedPostNumbers.length === 0) { + return {} as Record; + } + + const { numberToCid } = usePostNumberStore.getState(); + const nextQuotedNumberToCid: Record = {}; + + for (const postNumber of quotedPostNumbers) { + const quotedCid = numberToCid[postNumber]; + if (quotedCid) { + nextQuotedNumberToCid[postNumber] = quotedCid; + } + } + + return nextQuotedNumberToCid; + }, [quotedPostNumbers, quotedNumbersSignature]); + + return useMemo(() => { + const map = new Map(); + + for (const { reply, quotedPostNumbers: quotedNumbersForReply } of replyQuoteTargets) { + const cidSet = new Set(); + + if (reply.quotedCids?.length) { + for (const quotedCid of reply.quotedCids) { + cidSet.add(quotedCid); + } + } + + for (const postNumber of quotedNumbersForReply) { + const quotedCid = quotedNumberToCid[postNumber]; + if (quotedCid) { + cidSet.add(quotedCid); + } + } + + for (const quotedCid of cidSet) { + const repliesQuotingCid = map.get(quotedCid); + if (repliesQuotingCid) { + repliesQuotingCid.push(reply); + } else { + map.set(quotedCid, [reply]); + } + } + } + + return map; + }, [replyQuoteTargets, quotedNumberToCid]); +}; + +export default useQuotedByMap; diff --git a/src/stores/use-post-number-store.ts b/src/stores/use-post-number-store.ts index d5b80bc7..5a494bf0 100644 --- a/src/stores/use-post-number-store.ts +++ b/src/stores/use-post-number-store.ts @@ -14,18 +14,28 @@ const usePostNumberStore = create((set) => ({ if (!comments?.length) return; set((state) => { - const nextNumberToCid = { ...state.numberToCid }; - const nextCidToNumber = { ...state.cidToNumber }; + let nextNumberToCid = state.numberToCid; + let nextCidToNumber = state.cidToNumber; + let hasUpdates = false; for (const c of comments) { const num = c?.number; const cid = c?.cid; - if (typeof num === 'number' && cid) { + if (typeof num === 'number' && cid && (nextNumberToCid[num] !== cid || nextCidToNumber[cid] !== num)) { + if (!hasUpdates) { + nextNumberToCid = { ...state.numberToCid }; + nextCidToNumber = { ...state.cidToNumber }; + hasUpdates = true; + } nextNumberToCid[num] = cid; nextCidToNumber[cid] = num; } } + if (!hasUpdates) { + return state; + } + return { numberToCid: nextNumberToCid, cidToNumber: nextCidToNumber }; }); },