Fix codebase audit regressions while preserving UI/UX behavior and adding review-driven hardening.
20 KiB
Dead Code & Duplication Audit
Summary
The codebase is clean of obvious dead code. There are no TODO/FIXME/HACK/XXX markers in any .ts/.tsx file under src/ (outside the skipped src/generated/ and src/e2e/ trees), no @deprecated tags, no commented-out code blocks, and no if (false) / unreachable branches. Knip's output is short and all its findings verify as real, though two are "tested-but-not-used" orphans rather than truly dead symbols.
The genuine signal is concentrated in two places:
- A small handful of exports that are only referenced from their own unit tests (archive barrel re-export,
useAccountCommunitiesWithMetadata,isArchiveView/isSettingsView/isNotFoundView,isDirectMediaUrl,getPreferredOrder/getRandomOrder,getCatalogPostHeightEstimate/getCatalogRowHeightEstimate). Safe to either delete, make non-exported, or wire up a real consumer. - Duplicated logic, not duplicated files. The big offenders are
post-desktop.tsx/post-mobile.tsx(they sharePendingModerationActions, a moderation-options block repeated again inmod-queue.tsx, a manualisAccountModcomputation that already has a hook, and the.eth/.soldisplay-name switch), plus theuseEditCommentPrivilegeshook carrying an unusedpostCidparameter.
Outside those, there is one stale duplicate in package.json (cross-env listed in both dependencies and devDependencies) and one stale type shim (declare module 'react-draggable' for a package that is no longer installed). Total impact is measured in a handful of files — there is no bit-rot backlog here, just a cleanup pass.
knip Findings
yarn knip and yarn knip:full produced identical, short output (13 reportable lines, none --no-exit-code would have suppressed):
| Knip category | Finding | Verification |
|---|---|---|
| Unused files | src/views/archive/index.ts |
confirmed. The file is a barrel export { default } from './archive';. src/app.tsx:39 imports ./views/archive/archive directly, unlike every other view under src/views/ which all go through their index.ts. |
| Unused devDependencies | cross-env in package.json:148 |
confirmed. cross-env is legitimately used in build, build-vercel, analyze-bundle, and both electron:start* scripts, but it is declared twice — once at package.json:25 (dependencies) and once at package.json:148 (devDependencies). The devDependencies copy is redundant. |
| Unused exports | getCommunityIdentifier in src/hooks/use-community-identifiers.ts:7 |
confirmed. Only consumed by the sibling getCommunityIdentifiers and useCommunityIdentifier in the same file. Not re-exported externally. |
| Unused exports | getCommunityIdentifiers in src/hooks/use-community-identifiers.ts:39 |
confirmed. Only consumed by the sibling useCommunityIdentifiers in the same file. A same-named local getCommunityIdentifiers exists in src/lib/utils/post-page-resolution.ts:67 (different signature) — knip is not confused, the module-level export really is internal-only. |
| Unused exports | REPLY_HEIGHT_DATA_ATTRIBUTE in src/lib/utils/pretext-height-estimates.ts:71 |
confirmed. Used once inside the same file at line 853 as a DOM selector. No external consumer. |
| Unused exports | getCatalogPostHeightEstimate in src/lib/utils/pretext-height-estimates.ts:746 |
confirmed. Only called internally at line 789 from getCatalogRowHeightEstimate. |
| Unused exports | getCatalogRowHeightEstimate in src/lib/utils/pretext-height-estimates.ts:780 |
confirmed. Only called internally at line 804 from getCatalogRowHeightEstimates. |
| Unused exported types | ArchiveRouteRenderOptions in src/views/archive/__tests__/helpers.ts:10 |
confirmed. The helper renderArchiveRoute is called with object-literal args at 5 sites in archive.test.tsx; none of the test files name the type. The export keyword on the type alias is surplus. |
| Configuration hints | android/app/src/main/assets/public/** still in ignoreFiles |
likely-false-positive (harness-specific). This path is listed as a deliberate skip for Android-bundled build output. Knip is offering to remove it because the directory doesn't currently exist in the checkout; leave it pinned unless the Android workflow has been retired. |
| Configuration hints | babel-plugin-react-compiler still in ignoreDependencies |
likely-false-positive (build tooling). It is loaded by Vite config as a string id and knip cannot trace it; the ignore entry is intentional. |
Two categories knip did not surface but that were checked by hand and are real: test-only orphans (useAccountCommunitiesWithMetadata, isDirectMediaUrl, etc.) and type-stub drift (declare module 'react-draggable'). Knip skips the first because tests import them, and the second because it only inspects JS/TS imports, not ambient .d.ts declarations for absent packages.
Findings
Unused Exports
- src/views/archive/index.ts:1 — One-line barrel
export { default } from './archive'that nobody imports;src/app.tsxuses./views/archive/archivedirectly. Action: either deleteindex.tsand leave the direct import, or flipapp.tsxtoimport Archive from './views/archive'(the convention used by every other view). The second is the smaller-surface fix.confirmed. - src/hooks/use-community-identifiers.ts:7,39 —
getCommunityIdentifierandgetCommunityIdentifiersare exported but only used by the React-facinguseCommunityIdentifier/useCommunityIdentifiersin the same file. Action: drop theexportkeyword on both; keep them as module-private helpers.confirmed. - src/lib/utils/pretext-height-estimates.ts:71 —
REPLY_HEIGHT_DATA_ATTRIBUTEis only used inside the same file (line 853). Action: drop theexport.confirmed. - src/lib/utils/pretext-height-estimates.ts:746,780 —
getCatalogPostHeightEstimateandgetCatalogRowHeightEstimateare exported but only used within the file bygetCatalogRowHeightEstimate/getCatalogRowHeightEstimatesrespectively. Action: drop theexporton both. OnlygetCatalogRowHeightEstimates(line 799) needs to remain public.confirmed. - src/views/archive/tests/helpers.ts:10 —
export type ArchiveRouteRenderOptionsis never referenced by name; callers pass object literals. Action: drop theexportkeyword.confirmed. - src/hooks/use-account-communities-with-metadata.ts:5 —
useAccountCommunitiesWithMetadatahas zero production consumers; onlysrc/hooks/__tests__/selector-hooks.test.tsx:6imports it. Action: delete the hook and its test cases, or wire it into the one plausible consumer (BoardsBar's "my communities" section currently usesuseDirectoriesinstead). Knip didn't flag this because the test keeps the import alive.confirmed. - src/lib/media-hosting/direct-url.ts:5 —
isDirectMediaUrland theDIRECT_MEDIA_EXTENSIONSlist have no non-test consumer. The extension set is also a subset ofKNOWN_IMAGE_EXTENSIONS/KNOWN_VIDEO_EXTENSIONSinsrc/lib/utils/media-utils.ts:77-78, so the logic duplicates the classifier used bygetLinkMediaInfo. Action: deletesrc/lib/media-hosting/direct-url.tsand its test, or rewrite it as a thin re-export of a sharedclassifyUrl()helper backed by themedia-utilsextension lists.confirmed. - src/lib/media-hosting/provider-order.ts:5,10 —
getPreferredOrderandgetRandomOrderare exported only to be exercised by__tests__/provider-order.test.ts.getPreferredOrderhas no internal consumer either;getRandomOrderis called once at line 34 by the publicgetProviderOrder. Action: drop bothexportkeywords and rewrite the tests againstgetProviderOrderwith runtime+mode combinations that exercise the preferred and random branches.confirmed. - src/lib/utils/view-utils.ts:85,93,72 —
isArchiveView,isNotFoundView, andisSettingsVieware only used internally (isNotFoundViewcomposes the other two) and by__tests__/view-utils.test.ts. No component/route imports them. Action: ifNotFoundViewrouting is still useful, wireisNotFoundViewintosrc/app.tsx's route-resolution logic (it currently duplicates the negation inline). Otherwise drop theexporton all three and delete the tests.confirmed. - src/hooks/use-author-privileges.ts:11 — The destructured return includes
accountAuthorRolebut no external consumer reads it (the two callers inedit-menuandpost-menu-mobiledestructure onlyisAccountMod/isAccountCommentAuthor/isCommentAuthorMod/commentAuthorRole). Action: dropaccountAuthorRolefrom the returned object.confirmed. - src/hooks/use-author-privileges.ts:5-9 — The
AuthorPrivilegesPropsinterface declarespostCid?: stringbut the hook body never reads it;edit-menu/edit-menu.tsx:52passes apostCidin anyway. Action: delete thepostCidfield from the interface and thepostCid,from the destructuring at the edit-menu call site. Dead parameter.confirmed.
Dead Code
- src/modules.d.ts:8 —
declare module 'react-draggable'type stub for a package that is not installed (node_modules/react-draggabledoes not exist, noreact-draggableentry inpackage.json, zerofrom 'react-draggable'imports undersrc/). Action: delete the line.confirmed. - src/modules.d.ts:6 —
declare module 'lodash'is a permissive any-typed stub, but@types/lodashis installed (package.jsondeclares it at the top level andnode_modules/@types/lodash/package.jsonexists). The stub silently broadens types the installed package would otherwise provide. Action: delete the stub. If tsgo complains about a specific submodule import (e.g.lodash/capitalize), the correct fix is to import fromlodashor add a typed helper, not to re-shadow withany.confirmed. - package.json:148 —
cross-envduplicated indevDependencies(it is already a directdependenciesentry at line 25 at the same version). Action: remove the devDependencies line so the package surfaces in exactly one group.confirmed.
Nothing else in this category turned up: no TODO/FIXME/HACK/XXX markers, no commented-out logic, no @deprecated tags, no if (false) / unreachable default: branches, no empty .module.css files.
Duplication Candidates
- src/components/post-desktop/post-desktop.tsx:99-209 vs src/components/post-mobile/post-mobile.tsx:115-210 vs src/views/mod-queue/mod-queue.tsx:209-281 — The
usePublishCommentModeration(approve)+usePublishCommentModeration(reject)pair is set up three times with the sameonChallenge/onChallengeVerification/onErrortrio and the samehandleApprove/handleRejectdouble-confirm wrapper. Post-desktop additionally wraps it in aPendingModerationActionssubcomponent, which post-mobile redoes inline insidePostInfoAndMedia. Shared abstraction:useApproveRejectModeration({ cid, communityAddress, post })insrc/hooks/, returning{ approve, reject, approveState, approveError, rejectState, rejectError, handleApprove, handleReject }. Callers render the UI. The hook owns the twousePublishCommentModerationcalls, the shared onChallenge/onError, and thewindow.confirm(t('double_confirm'))guard. This would delete roughly 90 lines across the three call sites. - src/components/post-desktop/post-desktop.tsx:253-254 + 109-112 vs src/components/post-mobile/post-mobile.tsx:108-109 + 124-127 — Both files manually compute
const accountRole = roles?.[accountAddress]?.role; const isAccountMod = accountRole === 'admin' || accountRole === 'owner' || accountRole === 'moderator';. The exact sameisAccountModlogic already lives insrc/hooks/use-author-privileges.ts(which is already consumed byedit-menuandpost-menu-mobile). Shared abstraction: calluseAuthorPrivileges({ commentAuthorAddress: address, communityAddress })in both post components instead, destructuringisAccountMod. Delete the manual ternary. Bonus: the post components would also pick upuseCommunityField-level selector memoization instead of readingrolesoff a prop. - src/components/boards-bar/boards-bar.tsx:195, src/components/board-header/board-header.tsx:98, src/components/post-desktop/post-desktop.tsx:642,857, src/components/post-mobile/post-mobile.tsx:85, src/views/mod-queue/mod-queue.tsx:639 — Six sites inline the check
address.endsWith('.eth') || address.endsWith('.sol') ? address : getShortAddress(address)to decide whether a community address should be displayed verbatim or truncated. Two sites (post-desktop,post-mobile) do it twice. Shared abstraction:formatCommunityAddressForDisplay(address)insrc/lib/utils/string-utils.ts(or a tinyis-domain-like-addresspredicate used by the existinggetShortAddressfallback). When the TLD list grows (the community shortlist already includes.sol; IPNS keys are a separate branch), you update it once. - src/components/post-desktop/post-desktop.tsx:72-78 vs src/components/post-mobile/post-mobile.tsx:62-67 — Identical
RepliesFootercomponent (hasMore ? <div className={styles.stateString}><LoadingEllipsis string={loadingString}/></div> : null) redefined in each file.stylesresolves to the sameviews/post/post.module.cssin both. Shared abstraction: moveRepliesFooterintosrc/components/(e.g.src/components/post-desktop/replies-footer.tsxshared via a barrel) or directly inline it under a singlesrc/components/post-thread-parts/. - src/components/post-desktop/post-desktop.tsx:80-97 vs src/components/post-mobile/post-mobile.tsx:69-70 — Both files keep a module-level
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}plus (desktop-only) auseShowOmittedRepliesZustand store. Desktop also defines an equivalentsetShowOmittedRepliesstore inline. Shared abstraction: lift the scroll-snapshot map into a sharedsrc/lib/utils/virtuoso-scroll-memory.tshelper (saveSnapshot(key, state)/getSnapshot(key)) so both files consume the same keyed cache. This also avoids two per-instance module caches holding onto (possibly stale) snapshots. TheuseShowOmittedRepliesstore belongs insrc/stores/use-show-omitted-replies-store.tssince desktop and mobile are mutually exclusive at runtime anyway. - src/components/settings-modal/crypto-wallets-setting/crypto-wallets-setting.tsx:65 — Uses
navigator.clipboard.writeText(messageToSign)directly, bypassingcopyToClipboardfromsrc/lib/utils/clipboard-utils.tsthat every other copy site (error-display,post-menu-desktop,post-menu-mobile,url-utils) routes through.clipboard-utilshandles the Electron IPC bridge + fallback that this site silently skips. Action: replace withawait copyToClipboard(messageToSign)and keep the failure alert that sits around it. - src/lib/media-hosting/direct-url.ts:2 vs src/lib/utils/media-utils.ts:77-78 —
DIRECT_MEDIA_EXTENSIONS(with leading dots:.jpg, .jpeg, .png, ...) is a subset ofKNOWN_IMAGE_EXTENSIONS+KNOWN_VIDEO_EXTENSIONS(without leading dots:jpg, jpeg, png, ...). Two independently-maintained lists with different shapes, same intent. Shared abstraction: one canonical list inmedia-utils.tsplus aclassifyUrlAsDirectMedia(url)function that the rare caller needs. Resolves thedirect-url.tsfile's dead-code status at the same time.
TODO/FIXME Backlog
Empty. grep -rn -i 'TODO\|FIXME\|HACK\|XXX' under src/ (excluding src/generated/ and src/e2e/) returns zero markers in any .ts, .tsx, .js, .jsx, or .css file. The only hit is a narrative comment in src/app.tsx:126 containing the literal substring "xxx" ("/thread/xxx") — not a marker. Nothing to triage.
Legacy Code
- src/modules.d.ts:8 (
react-draggable) — Already flagged under Dead Code. This is the only remaining ambient type stub for a package that isn't installed. The two siblings in the file (*.module.css,react-router-hash-link) are still needed:@types/react-router-hash-linkis not published on DefinitelyTyped, and CSS-modules typing is a Vite-compatible pattern. - src/modules.d.ts:6 (
declare module 'lodash') — Already flagged under Dead Code.@types/lodash@4.17.24is installed indevDependenciesand provides full typings; the ambientdeclare module 'lodash'shadows them with implicitany. Pure legacy residue. - src/lib/utils/view-utils.ts:81,34,93,85,72 — Several predicate signatures declare
params: ParamsTypebut don't read from it (isSubscriptionsView, and the bodies ofisCatalogView/isSettingsViewonly readboardIdentifier/commentCid/accountCommentIndex).isSubscriptionsViewin particular has 12 call sites all passingparamsneedlessly. This is light code smell, not dead code — trimming is optional. Action (optional): drop the unusedparamsfromisSubscriptionsView; leave the others (they do read fromparams). - src/data/5chan-blotter.json (not opened here, flagged for completeness) — The blotter data file is the source of truth for the blotter UI; not legacy even though it's auto-generated. Left in place.
No @deprecated JSDoc tags, no commented-out code blocks, no feature-flag branches that no longer toggle were found. The codebase has been cleaned recently; the backlog is genuinely thin.
Top 5 Actions
- Delete the
react-draggabletype stub and the duplicatecross-envdevDep. Two-line net removal insrc/modules.d.ts:8andpackage.json:148, zero risk, and it clears the only pieces of stale manifest/stub drift. - Un-export internal helpers flagged by knip. Drop the
exportkeyword ongetCommunityIdentifier/getCommunityIdentifiers(src/hooks/use-community-identifiers.ts:7,39),REPLY_HEIGHT_DATA_ATTRIBUTE(src/lib/utils/pretext-height-estimates.ts:71),getCatalogPostHeightEstimate/getCatalogRowHeightEstimate(pretext-height-estimates.ts:746,780), andArchiveRouteRenderOptions(src/views/archive/__tests__/helpers.ts:10). Also drop theexportongetPreferredOrder/getRandomOrder(src/lib/media-hosting/provider-order.ts:5,10) and rewrite their tests to go throughgetProviderOrder. Shrinks the public API surface by ~7 symbols without changing behavior. - Extract
useApproveRejectModeration({ cid, communityAddress, post })and collapse the three copies. Replaces theusePublishCommentModeration(approve)+usePublishCommentModeration(reject)+handleApprove/handleRejectblocks inpost-desktop.tsx:102-159,post-mobile.tsx:115-210, andmod-queue.tsx:209-281. Roughly 90 LOC deleted, the onChallenge/onChallengeVerification/onError error-log strings stop drifting, and thewindow.confirm(t('double_confirm'))gate has one owner. - Make the post components reuse
useAuthorPrivilegesand delete thepostCidghost parameter. Replace the manualisAccountModternaries inpost-desktop.tsx:253-254andpost-mobile.tsx:108-109with auseAuthorPrivileges({ commentAuthorAddress, communityAddress })call. Simultaneously droppostCidfromAuthorPrivilegesPropsinsrc/hooks/use-author-privileges.ts:5-9and from the caller inedit-menu.tsx:52. Removes two duplicated logic sites and kills an unused parameter. - Delete or wire up the four test-only orphans. Either delete (preferred if there is no plan to use them) or land a production consumer for:
src/hooks/use-account-communities-with-metadata.ts(plus its test inselector-hooks.test.tsx),src/lib/media-hosting/direct-url.ts(plus its test; merge intomedia-utils.ts's extension lists if a use case returns),isArchiveView/isNotFoundView/isSettingsViewinsrc/lib/utils/view-utils.ts(plus their tests; or wireisNotFoundViewintoapp.tsxas the canonical not-found predicate), andsrc/views/archive/index.ts(either delete, or flipapp.tsx:39to import through it like every other view).