# 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:

1. 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.
2. Duplicated logic, not duplicated files. The big offenders are `post-desktop.tsx` / `post-mobile.tsx` (they share `PendingModerationActions`, a moderation-options block repeated again in `mod-queue.tsx`, a manual `isAccountMod` computation that already has a hook, and the `.eth`/`.sol` display-name switch), plus the `useEditCommentPrivileges` hook carrying an unused `postCid` parameter.

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.tsx` uses `./views/archive/archive` directly. *Action:* either delete `index.ts` and leave the direct import, or flip `app.tsx` to `import 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** — `getCommunityIdentifier` and `getCommunityIdentifiers` are exported but only used by the React-facing `useCommunityIdentifier` / `useCommunityIdentifiers` in the same file. *Action:* drop the `export` keyword on both; keep them as module-private helpers. `confirmed`.
- **src/lib/utils/pretext-height-estimates.ts:71** — `REPLY_HEIGHT_DATA_ATTRIBUTE` is only used inside the same file (line 853). *Action:* drop the `export`. `confirmed`.
- **src/lib/utils/pretext-height-estimates.ts:746,780** — `getCatalogPostHeightEstimate` and `getCatalogRowHeightEstimate` are exported but only used within the file by `getCatalogRowHeightEstimate` / `getCatalogRowHeightEstimates` respectively. *Action:* drop the `export` on both. Only `getCatalogRowHeightEstimates` (line 799) needs to remain public. `confirmed`.
- **src/views/archive/__tests__/helpers.ts:10** — `export type ArchiveRouteRenderOptions` is never referenced by name; callers pass object literals. *Action:* drop the `export` keyword. `confirmed`.
- **src/hooks/use-account-communities-with-metadata.ts:5** — `useAccountCommunitiesWithMetadata` has zero production consumers; only `src/hooks/__tests__/selector-hooks.test.tsx:6` imports it. *Action:* delete the hook and its test cases, or wire it into the one plausible consumer (`BoardsBar`'s "my communities" section currently uses `useDirectories` instead). Knip didn't flag this because the test keeps the import alive. `confirmed`.
- **src/lib/media-hosting/direct-url.ts:5** — `isDirectMediaUrl` and the `DIRECT_MEDIA_EXTENSIONS` list have no non-test consumer. The extension set is also a subset of `KNOWN_IMAGE_EXTENSIONS`/`KNOWN_VIDEO_EXTENSIONS` in `src/lib/utils/media-utils.ts:77-78`, so the logic duplicates the classifier used by `getLinkMediaInfo`. *Action:* delete `src/lib/media-hosting/direct-url.ts` and its test, or rewrite it as a thin re-export of a shared `classifyUrl()` helper backed by the `media-utils` extension lists. `confirmed`.
- **src/lib/media-hosting/provider-order.ts:5,10** — `getPreferredOrder` and `getRandomOrder` are exported only to be exercised by `__tests__/provider-order.test.ts`. `getPreferredOrder` has no internal consumer either; `getRandomOrder` is called once at line 34 by the public `getProviderOrder`. *Action:* drop both `export` keywords and rewrite the tests against `getProviderOrder` with runtime+mode combinations that exercise the preferred and random branches. `confirmed`.
- **src/lib/utils/view-utils.ts:85,93,72** — `isArchiveView`, `isNotFoundView`, and `isSettingsView` are only used internally (`isNotFoundView` composes the other two) and by `__tests__/view-utils.test.ts`. No component/route imports them. *Action:* if `NotFoundView` routing is still useful, wire `isNotFoundView` into `src/app.tsx`'s route-resolution logic (it currently duplicates the negation inline). Otherwise drop the `export` on all three and delete the tests. `confirmed`.
- **src/hooks/use-author-privileges.ts:11** — The destructured return includes `accountAuthorRole` but no external consumer reads it (the two callers in `edit-menu` and `post-menu-mobile` destructure only `isAccountMod`/`isAccountCommentAuthor`/`isCommentAuthorMod`/`commentAuthorRole`). *Action:* drop `accountAuthorRole` from the returned object. `confirmed`.
- **src/hooks/use-author-privileges.ts:5-9** — The `AuthorPrivilegesProps` interface declares `postCid?: string` but the hook body never reads it; `edit-menu/edit-menu.tsx:52` passes a `postCid` in anyway. *Action:* delete the `postCid` field from the interface and the `postCid,` 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-draggable` does not exist, no `react-draggable` entry in `package.json`, zero `from 'react-draggable'` imports under `src/`). *Action:* delete the line. `confirmed`.
- **src/modules.d.ts:6** — `declare module 'lodash'` is a permissive any-typed stub, but `@types/lodash` **is** installed (`package.json` declares it at the top level and `node_modules/@types/lodash/package.json` exists). 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 from `lodash` or add a typed helper, not to re-shadow with `any`. `confirmed`.
- **package.json:148** — `cross-env` duplicated in `devDependencies` (it is already a direct `dependencies` entry 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 same `onChallenge`/`onChallengeVerification`/`onError` trio and the same `handleApprove`/`handleReject` double-confirm wrapper. Post-desktop additionally wraps it in a `PendingModerationActions` subcomponent, which post-mobile redoes inline inside `PostInfoAndMedia`. *Shared abstraction:* `useApproveRejectModeration({ cid, communityAddress, post })` in `src/hooks/`, returning `{ approve, reject, approveState, approveError, rejectState, rejectError, handleApprove, handleReject }`. Callers render the UI. The hook owns the two `usePublishCommentModeration` calls, the shared onChallenge/onError, and the `window.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 same `isAccountMod` logic already lives in `src/hooks/use-author-privileges.ts` (which is already consumed by `edit-menu` and `post-menu-mobile`). *Shared abstraction:* call `useAuthorPrivileges({ commentAuthorAddress: address, communityAddress })` in both post components instead, destructuring `isAccountMod`. Delete the manual ternary. Bonus: the post components would also pick up `useCommunityField`-level selector memoization instead of reading `roles` off 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)` in `src/lib/utils/string-utils.ts` (or a tiny `is-domain-like-address` predicate used by the existing `getShortAddress` fallback). 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 `RepliesFooter` component (`hasMore ? <div className={styles.stateString}><LoadingEllipsis string={loadingString}/></div> : null`) redefined in each file. `styles` resolves to the same `views/post/post.module.css` in both. *Shared abstraction:* move `RepliesFooter` into `src/components/` (e.g. `src/components/post-desktop/replies-footer.tsx` shared via a barrel) or directly inline it under a single `src/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) a `useShowOmittedReplies` Zustand store. Desktop also defines an equivalent `setShowOmittedReplies` store inline. *Shared abstraction:* lift the scroll-snapshot map into a shared `src/lib/utils/virtuoso-scroll-memory.ts` helper (`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. The `useShowOmittedReplies` store belongs in `src/stores/use-show-omitted-replies-store.ts` since 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, bypassing `copyToClipboard` from `src/lib/utils/clipboard-utils.ts` that every other copy site (`error-display`, `post-menu-desktop`, `post-menu-mobile`, `url-utils`) routes through. `clipboard-utils` handles the Electron IPC bridge + fallback that this site silently skips. *Action:* replace with `await 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 of `KNOWN_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 in `media-utils.ts` plus a `classifyUrlAsDirectMedia(url)` function that the rare caller needs. Resolves the `direct-url.ts` file'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-link` is 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.24` is installed in `devDependencies` and provides full typings; the ambient `declare module 'lodash'` shadows them with implicit `any`. Pure legacy residue.
- **src/lib/utils/view-utils.ts:81,34,93,85,72** — Several predicate signatures declare `params: ParamsType` but don't read from it (`isSubscriptionsView`, and the bodies of `isCatalogView`/`isSettingsView` only read `boardIdentifier` / `commentCid` / `accountCommentIndex`). `isSubscriptionsView` in particular has 12 call sites all passing `params` needlessly. This is light code smell, not dead code — trimming is optional. *Action (optional):* drop the unused `params` from `isSubscriptionsView`; leave the others (they do read from `params`).
- **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

1. **Delete the `react-draggable` type stub and the duplicate `cross-env` devDep.** Two-line net removal in `src/modules.d.ts:8` and `package.json:148`, zero risk, and it clears the only pieces of stale manifest/stub drift.
2. **Un-export internal helpers flagged by knip.** Drop the `export` keyword on `getCommunityIdentifier`/`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`), and `ArchiveRouteRenderOptions` (`src/views/archive/__tests__/helpers.ts:10`). Also drop the `export` on `getPreferredOrder`/`getRandomOrder` (`src/lib/media-hosting/provider-order.ts:5,10`) and rewrite their tests to go through `getProviderOrder`. Shrinks the public API surface by ~7 symbols without changing behavior.
3. **Extract `useApproveRejectModeration({ cid, communityAddress, post })` and collapse the three copies.** Replaces the `usePublishCommentModeration(approve)` + `usePublishCommentModeration(reject)` + `handleApprove`/`handleReject` blocks in `post-desktop.tsx:102-159`, `post-mobile.tsx:115-210`, and `mod-queue.tsx:209-281`. Roughly 90 LOC deleted, the onChallenge/onChallengeVerification/onError error-log strings stop drifting, and the `window.confirm(t('double_confirm'))` gate has one owner.
4. **Make the post components reuse `useAuthorPrivileges` and delete the `postCid` ghost parameter.** Replace the manual `isAccountMod` ternaries in `post-desktop.tsx:253-254` and `post-mobile.tsx:108-109` with a `useAuthorPrivileges({ commentAuthorAddress, communityAddress })` call. Simultaneously drop `postCid` from `AuthorPrivilegesProps` in `src/hooks/use-author-privileges.ts:5-9` and from the caller in `edit-menu.tsx:52`. Removes two duplicated logic sites and kills an unused parameter.
5. **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 in `selector-hooks.test.tsx`), `src/lib/media-hosting/direct-url.ts` (plus its test; merge into `media-utils.ts`'s extension lists if a use case returns), `isArchiveView`/`isNotFoundView`/`isSettingsView` in `src/lib/utils/view-utils.ts` (plus their tests; or wire `isNotFoundView` into `app.tsx` as the canonical not-found predicate), and `src/views/archive/index.ts` (either delete, or flip `app.tsx:39` to import through it like every other view).
