From ac5d24e8235d1956007b0e86b69d38a652812c09 Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Fri, 11 Sep 2026 14:42:38 +0530 Subject: [PATCH 1/8] perf(posts): memoised replies, paginated comments, persistent drafts TokenComments filtered the whole list per top-level post (O(n^2) per render) and rendered every comment at once with a silent slice composer. Build the reply map once with groupReplies, paginate the top level at 20 with a Show more button and live status, persist the draft per token so a failed signature or reload never loses a typed comment, and announce the character count and errors to assistive tech. --- app/src/components/launchpad/Posts.tsx | 55 ++++++++++++++---- app/src/lib/launchpad/post-threads.test.ts | 38 +++++++++++++ app/src/lib/launchpad/post-threads.ts | 65 ++++++++++++++++++++++ 3 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 app/src/lib/launchpad/post-threads.test.ts create mode 100644 app/src/lib/launchpad/post-threads.ts diff --git a/app/src/components/launchpad/Posts.tsx b/app/src/components/launchpad/Posts.tsx index 69f8d90a..d6e68cca 100644 --- a/app/src/components/launchpad/Posts.tsx +++ b/app/src/components/launchpad/Posts.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useAccount, useConfig } from "wagmi"; import { getWalletClient } from "wagmi/actions"; import WalletAvatar from "@/components/WalletAvatar"; @@ -10,6 +10,7 @@ import { useLive } from "./LiveProvider"; import { toast } from "./TxToasts"; import { btn } from "@/components/ui"; import { buildModMessage, buildPostMessage, buildReportMessage, POST_MAX, REPORT_REASONS, validateBody, type ReportReason } from "@/lib/launchpad/posts"; +import { COMMENTS_PAGE, clearDraft, groupReplies, loadDraft, saveDraft, visibleTopIds } from "@/lib/launchpad/post-threads"; import type { PostRow } from "@/lib/launchpad/postsServer"; import { ago, nowMs } from "@/lib/launchpad/time"; import { CHAINS, CHAIN_SHORT, shortAddr, type ChainKey } from "@/lib/chainPublic"; @@ -51,6 +52,16 @@ export default function TokenComments({ chain, token, symbol, launcher, embedded const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const [now, setNow] = useState(0); + const [shownCount, setShownCount] = useState(COMMENTS_PAGE); + // Draft restores async after hydration so server and client render the same empty composer first. + useEffect(() => { + const t = setTimeout(() => { + const d = loadDraft(chain, token); + if (d) setBody(d.slice(0, POST_MAX)); + setShownCount(COMMENTS_PAGE); + }, 0); + return () => clearTimeout(t); + }, [chain, token]); const seenPosts = useRef(""); const isCreator = Boolean(address && address.toLowerCase() === launcher.toLowerCase()); @@ -104,6 +115,7 @@ export default function TokenComments({ chain, token, symbol, launcher, embedded if (!res.ok || !d.post) throw new Error(d.error ?? "post rejected"); setPosts((cur) => [d.post!, ...cur]); setBody(""); + clearDraft(chain, token); setReplyTo(null); } catch (e) { setErr(friendlyError(e)); @@ -145,8 +157,9 @@ export default function TokenComments({ chain, token, symbol, launcher, embedded } } - const top = posts.filter((p) => p.parent_id === null); - const replies = (id: number) => posts.filter((p) => p.parent_id === id).slice().reverse(); + const { top, repliesById } = useMemo(() => groupReplies(posts), [posts]); + const visibleTop = useMemo(() => visibleTopIds(top, shownCount), [top, shownCount]); + const remaining = top.length - visibleTop.length; return (
@@ -182,29 +195,37 @@ export default function TokenComments({ chain, token, symbol, launcher, embedded ) : null}