Skip to content

fix(electron): use bundled Chromium version in user agent#1129

Merged
tomcasaburi merged 8 commits into
masterfrom
codex/fix/linux-turnstile-user-agent
May 14, 2026
Merged

fix(electron): use bundled Chromium version in user agent#1129
tomcasaburi merged 8 commits into
masterfrom
codex/fix/linux-turnstile-user-agent

Conversation

@tomcasaburi
Copy link
Copy Markdown
Member

@tomcasaburi tomcasaburi commented May 14, 2026

Summary

  • derive the Electron desktop user agent from the bundled Chromium major version instead of hardcoding Chrome 116
  • keep the existing platform-shaped desktop user agents for Windows, macOS, and Linux
  • add focused coverage for Linux iframe navigation UA generation

Root Cause

The Linux desktop app in 0.8.4 advertised Chrome/116.0.0.0 even though the bundled Electron runtime is much newer. The live spam-blocker server showed affected attempts loading the iframe and later calling /challenge/verify without any /challenge/complete request, which means the Turnstile widget did not produce a successful completion callback before the user submitted.

Validation

  • corepack yarn vitest run electron/user-agent.test.js
  • corepack yarn test --run
  • corepack yarn lint
  • corepack yarn type-check
  • corepack yarn knip
  • corepack yarn build

Note

Medium Risk
Medium risk because it changes Electron request headers (user agent) and modifies feed/catalog rendering (hidden threads toggle, hidden post masking, and media error UI), which could affect navigation and content display across platforms.

Overview
Electron desktop user agent is now generated from the bundled Chromium major version via new electron/user-agent.js, replacing the previously hardcoded Chrome 116 UA string in electron/main.js (with tests to cover platform variants and fallback behavior).

Refactors PKC RPC startup by extracting server creation/logging into start-pkc-rpc-core.js and wiring start-pkc-rpc.js through it, adding a focused unit test.

Improves catalog/feed UX around hidden and failed media: adds a hidden-thread count + Show/Back toggle in board buttons (with supporting useHiddenCatalogThreads coverage), allows catalog rows to optionally render hidden post content, and replaces the “file deleted” fallback with a localized media-load failure warning + “open original” link; CommentContent gains appendContent to surface this guidance under posts on mobile. Also adjusts board blotter rendering rules and updates translations for the new strings and a few existing labels.

Reviewed by Cursor Bugbot for commit 6c68732. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added hidden catalog threads toggle for managing board visibility
    • Added detailed error messages when external media fails to load
    • Updated P2P gateway mode references throughout the interface
  • Bug Fixes

    • Improved board blotter rendering on archive and settings routes
  • Localization

    • Updated translations across 40+ languages for Pass messaging and media error handling

Review Change Stack

# Conflicts:
#	public/translations/ar/default.json
#	public/translations/bn/default.json
#	public/translations/cs/default.json
#	public/translations/da/default.json
#	public/translations/de/default.json
#	public/translations/el/default.json
#	public/translations/en/default.json
#	public/translations/es/default.json
#	public/translations/fa/default.json
#	public/translations/fi/default.json
#	public/translations/fil/default.json
#	public/translations/fr/default.json
#	public/translations/he/default.json
#	public/translations/hi/default.json
#	public/translations/hu/default.json
#	public/translations/id/default.json
#	public/translations/it/default.json
#	public/translations/ja/default.json
#	public/translations/ko/default.json
#	public/translations/mr/default.json
#	public/translations/nl/default.json
#	public/translations/no/default.json
#	public/translations/pl/default.json
#	public/translations/pt/default.json
#	public/translations/ro/default.json
#	public/translations/ru/default.json
#	public/translations/sq/default.json
#	public/translations/sv/default.json
#	public/translations/te/default.json
#	public/translations/th/default.json
#	public/translations/tr/default.json
#	public/translations/uk/default.json
#	public/translations/ur/default.json
#	public/translations/vi/default.json
#	public/translations/zh/default.json
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
5chan Ready Ready Preview, Comment May 14, 2026 11:10am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR implements hidden catalog threads—allowing users to block and selectively view hidden comments—while adding media load failure messaging throughout the UI. Hidden state is now backed by account data instead of local storage. The catalog can toggle between showing visible and hidden-only threads. Media load failures display inline with host-specific guidance and VPN tips. Supporting changes refactor Electron user-agent generation, extract PKC RPC setup, and simplify board blotter logic. Translations are updated across 40+ languages.

Changes

Hidden Catalog Threads with Media Failure Support

Layer / File(s) Summary
Hidden thread state store and core hooks
src/stores/use-hidden-catalog-threads-store.ts, src/hooks/use-hide.ts
Zustand store tracks hidden comments by CID and scope; useHide refactored to read blockedCids from account and persist comment context for catalog resolution.
Hidden thread computation and pruning
src/hooks/use-hidden-catalog-threads.ts, src/hooks/use-prune-hidden-catalog-threads.ts
New hooks compute hidden threads from blocked CIDs with directory scoping/sorting, and automatically unblock threads no longer in the fully-loaded board catalog.
Post menu context for hide actions
src/lib/utils/post-menu-props.ts
PostMenuProps now includes the comment being hidden so hide/unhide can record full context in the store.
Catalog view hidden thread filtering and display
src/views/catalog/catalog.tsx
Main catalog integration: filters feeds by hidden CIDs, toggles between visible and hidden-only modes, adjusts footer/pagination/Virtuoso state for hidden context.
Board buttons toggle and catalog row rendering
src/components/board-buttons/board-buttons.tsx, src/components/board-buttons/board-buttons.module.css, src/components/catalog-row/catalog-row.tsx
Adds HiddenCatalogThreadsToggle to show/hide hidden threads; CatalogRow/CatalogPost render with showHiddenPosts prop to mask/unmask content.
Media load failure components and styling
src/components/comment-media/comment-media.tsx, src/components/comment-media/comment-media.module.css, src/components/comment-media/index.ts
Introduces MediaLoadFailureInfo component with host-specific translations for displaying when external images fail to load.
Post and comment views media failure integration
src/components/post-mobile/post-mobile.tsx, src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx, src/components/post-mobile/post-menu-mobile/post-menu-mobile.tsx
Integrates media load failure tracking via onMediaLoadFailureChange callback; surfaces failure info in CommentContent via appendContent prop.
Comment content appendContent plumbing
src/components/comment-content/comment-content.tsx
Adds optional appendContent prop to enable composable UI injection after markdown (e.g., media failures).
Tests for hidden catalog threads
src/hooks/__tests__/use-hidden-catalog-threads.test.tsx, src/hooks/__tests__/use-hide.test.tsx, src/hooks/__tests__/use-prune-hidden-catalog-threads.test.tsx, src/views/catalog/__tests__/catalog.test.tsx, src/components/board-buttons/__tests__/board-buttons.test.tsx, src/components/catalog-row/__tests__/catalog-row.test.tsx, src/components/comment-media/__tests__/comment-media.test.tsx, src/components/comment-content/__tests__/comment-content.test.tsx
Comprehensive suites verify hidden-thread computation, blocking/unblocking, catalog filtering, UI toggling, multiboard scope handling, and media failure rendering.

Electron and Server Infrastructure Updates

Layer / File(s) Summary
Dynamic user-agent generation helpers
electron/user-agent.js, electron/user-agent.test.js, electron/main.js
Introduces getChromeUserAgentVersion and createFakeUserAgent to dynamically generate Chrome-like user-agent strings, replacing hardcoded platform-specific strings.
PKC RPC server extraction
electron/start-pkc-rpc-core.js, electron/start-pkc-rpc-core.test.js, electron/start-pkc-rpc.js
Extracts startPkcRpcServer function to consolidate WebSocket server setup, error logging, and dev-mode message handling for reusability.
Board blotter rendering simplification
src/app.tsx, src/__tests__/app.test.tsx
Centralizes desktop board blotter visibility into shouldRenderBoardBlotter flag; simplifies rule to show unless archive or mod-queue route.

Multilingual UI Messaging Updates

Layer / File(s) Summary
Translation key updates across 40+ languages
public/translations/{ar,bn,cs,da,de,el,en,es,fa,fi,fil,fr,he,hi,hu,id,it,ja,ko,mr,nl,no,pl,pt,ro,ru,sq,sv,te,th,tr,uk,ur,vi,zh}/default.json
Updates pass_heading and enable_pure_p2p_tip phrasing; adds comprehensive media_failed_to_load* keys with host-specific messages, VPN guidance, and inline variants.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • bitsocialnet/5chan#1121: Modifies Virtuoso virtualization in catalog.tsx for single-board vs multiboard contexts; main PR adjusts Virtuoso state preservation for hidden-thread modes.
  • bitsocialnet/5chan#1050: Shares thread scroll-to-top utility logic and outer OP container scrolling behavior.
  • bitsocialnet/5chan#1095: Both PRs introduce/use asset URL resolution helpers for packaged Electron assets via import.meta.env.BASE_URL.

Poem

🐰 Hidden threads now find their rest,
In account-backed caches, quietly blessed.
Media fails? We show the way—
Toggle threads, then hide away!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix/linux-turnstile-user-agent

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6c68732. Configure here.

@@ -0,0 +1,16 @@
export const startPkcRpcServer = async ({ PKCRpcModule, port, pkcOptions, authKey, isDev = false, logger = console }) => {
const pkcWebSocketServer = await PKCRpcModule.PKCWsServer({ port, pkcOptions, authKey });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PKC options property renamed, may break RPC server

High Severity

The old code passed configuration to PKCWsServer as { port, pkc: defaultPkcOptions, authKey }, but the refactored startPkcRpcServer now passes it as { port, pkcOptions, authKey }. If the underlying @pkcprotocol/pkc-js/rpc library expects the property name pkc, the server will silently start without its data path or IPFS connection options, rendering the desktop app unable to load boards or comments. The test only validates the wrapper's passthrough against a mock, not the real library's expected API shape.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6c68732. Configure here.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (4)
src/hooks/use-hide.ts (1)

68-86: 💤 Low value

Asymmetric error recovery in unhide vs hide.

hide reverts the optimistic rememberHiddenComment when blockCid rejects and the account state confirms still-not-hidden (Lines 58-64). unhide calls forgetHiddenComment(cid) up-front (Line 73) but never re-remembers on unblockCid rejection — so a failed unblock leaves hidden === true (account state unchanged) with no in-memory Comment for the catalog to display until useComments re-fetches. Consider restoring the remembered comment on failure to mirror hide's rollback, e.g.:

♻️ Suggested rollback
-  const unhide = useCallback(() => {
+  const unhide = useCallback(() => {
     if (!cid) {
       return;
     }

     forgetHiddenComment(cid);
     if (!getCurrentAccountHiddenState(cid)) {
       return;
     }

     void accountsStore
       .getState()
       .accountsActions.unblockCid(cid)
       .catch((error: unknown) => {
+        if (getCurrentAccountHiddenState(cid)) {
+          rememberHiddenComment(comment);
+        }
         if (shouldLogHideActionError(cid, false)) {
           console.error('Failed to unhide post', error);
         }
       });
-  }, [cid, forgetHiddenComment]);
+  }, [cid, comment, forgetHiddenComment, rememberHiddenComment]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/use-hide.ts` around lines 68 - 86, The unhide function currently
removes the optimistic in-memory entry via forgetHiddenComment(cid) before
calling accountsStore.getState().accountsActions.unblockCid and does not restore
it on failure; update unhide to mirror hide's rollback: after catching errors
from unblockCid, check if getCurrentAccountHiddenState(cid) still indicates
hidden and, if so, call rememberHiddenComment(cid) to re-create the in-memory
comment (only when shouldLogHideActionError or the account state requires it),
ensuring the optimistic removal is reverted on unblock failures; reference
unhide, forgetHiddenComment, rememberHiddenComment,
accountsStore.getState().accountsActions.unblockCid, shouldLogHideActionError,
and getCurrentAccountHiddenState when making the change.
src/hooks/use-hidden-catalog-threads.ts (1)

167-183: ⚡ Quick win

Default candidateComments = [] creates a new array every call, invalidating downstream memos.

When callers omit candidateComments, the destructuring default [] allocates a fresh array on every render, so the candidateCommentList useMemo at Lines 180-183 (which lists candidateComments in its deps) recomputes every render — cascading through hiddenThreadCandidates and hiddenCatalogThreads. Use a module-level stable empty array:

♻️ Stable default reference
+const EMPTY_CANDIDATE_COMMENTS: readonly Comment[] = Object.freeze([]) as readonly Comment[];
+
-const useHiddenCatalogThreads = ({ candidateComments = [], communityAddresses, sortType }: HiddenCatalogThreadsOptions): HiddenCatalogThreadsResult => {
+const useHiddenCatalogThreads = ({ candidateComments = EMPTY_CANDIDATE_COMMENTS, communityAddresses, sortType }: HiddenCatalogThreadsOptions): HiddenCatalogThreadsResult => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/use-hidden-catalog-threads.ts` around lines 167 - 183, The default
parameter candidateComments = [] in useHiddenCatalogThreads creates a new array
each render and forces downstream memos to recompute; fix by introducing a
module-level stable empty array (e.g., STABLE_EMPTY_CANDIDATE_COMMENTS:
Comment[] = []) and use that stable constant as the default for
candidateComments (or fall back inside the function) so candidateCommentList's
useMemo (which depends on candidateComments and computes
mergeCommentsByCid([...])) sees a stable reference and won't retrigger
unnecessarily.
src/hooks/use-prune-hidden-catalog-threads.ts (1)

89-89: 💤 Low value

Selector subscribes to the full communitiesPages map.

communitiesPagesStore((state) => state.communitiesPages) returns the entire pages map for all communities, so this hook re-runs whenever pages for any unrelated community update. Since downstream code only needs the entry keyed by the current community, narrow the selector — e.g. derive pages from communityAddress inside the selector, or memoize per community.posts?.pageCids — to keep this hook's re-render scope tight. The current pattern is functionally correct but unnecessarily broad.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/use-prune-hidden-catalog-threads.ts` at line 89, The selector
currently subscribes to the entire communitiesPages map via
communitiesPagesStore((state) => state.communitiesPages); change it so the
selector only returns the entry for the current community (use communityAddress
inside the selector) or directly derive the specific value needed (e.g.
community.posts?.pageCids) so the hook (usePruneHiddenCatalogThreads or the hook
body) only re-runs when that community's pages change; update the selector call
to access state.communitiesPages[communityAddress] or return the memoized
pageCids for that community instead of the whole map.
src/views/catalog/catalog.tsx (1)

397-409: ⚡ Quick win

Consider adding inline comments to clarify the auto-hide state machine.

This effect implements non-trivial logic to automatically dismiss the hidden-threads view when threads disappear. The guard at Line 406 (hadVisibleHiddenThreadsRef.current || !isLoadingHiddenCatalogThreads) handles two distinct cases:

  1. User toggled to hidden mode, saw threads, then they all disappeared
  2. User toggled to hidden mode while loading finished with zero results

A brief inline comment explaining this state machine would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/views/catalog/catalog.tsx` around lines 397 - 409, The effect in
useEffect controlling auto-hide of the hidden-threads panel is non-obvious; add
a concise inline comment above this block explaining the state machine: describe
that when requestedShowHiddenThreads is false we reset
hadVisibleHiddenThreadsRef, when hiddenThreadsCount > 0 we mark
hadVisibleHiddenThreadsRef.current = true, and when either
hadVisibleHiddenThreadsRef is true (threads were visible but then disappeared)
or loading finished with zero results (!isLoadingHiddenCatalogThreads) we clear
the shown scope via setShownHiddenThreadsScopeKey(null); reference the variables
useEffect, requestedShowHiddenThreads, hadVisibleHiddenThreadsRef,
hiddenThreadsCount, isLoadingHiddenCatalogThreads, and
setShownHiddenThreadsScopeKey in the comment so future maintainers understand
the two distinct cases handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/start-pkc-rpc-core.js`:
- Line 6: The log currently prints the full secret authKey (logger.log in
start-pkc-rpc-core.js), which leaks credentials; change the logging in the PKC
RPC startup to avoid printing authKey in plaintext—log only non-sensitive info
such as the port and a redacted or hashed representation of authKey (e.g., mask
all but last 4 chars or log "authKey=REDACTED"/its SHA256) while keeping the
existing message context so callers can still see the service is listening
without exposing the secret.

In `@public/translations/ar/default.json`:
- Line 377: Replace the English string for the translation key
"enable_pure_p2p_tip" with an Arabic translation so the UI is fully localized
for Arabic users; update the value of "enable_pure_p2p_tip" from "Use a browser
P2P node instead of gateway compatibility mode." to an appropriate Arabic text
(for example: "استخدم عقدة P2P في المتصفح بدلاً من وضع التوافق مع البوابة.")
ensuring the JSON string remains valid.

In `@public/translations/en/default.json`:
- Line 383: Replace the ungrammatical value for the "app_is_up_to_date" key:
change the string "you're on latest" to a properly capitalized and punctuated
phrase such as "You're on the latest version." in the JSON so the translation
reads correctly.

In `@public/translations/ro/default.json`:
- Line 377: The translation key "enable_pure_p2p_tip" is still in English in the
Romanian locale; replace the English value with an appropriate Romanian
translation for that key in public/translations/ro/default.json and apply the
corresponding translated text to the same key ("enable_pure_p2p_tip") in the
other locales mentioned (ru, sq, sv, te, th, tr, uk) so the UI is consistent
across languages; ensure you only change the value string (keep the JSON key
intact and maintain valid JSON quoting/commas).

In `@public/translations/ur/default.json`:
- Line 377: The key "enable_pure_p2p_tip" currently has an English string;
replace its value with an Urdu translation so the Urdu locale is consistent —
update the JSON value for "enable_pure_p2p_tip" to an appropriate Urdu phrase
such as "گیٹ وے کمپٹیبیلٹی موڈ کی بجائے براؤزر P2P نوڈ استعمال کریں۔" ensuring
proper JSON quoting and punctuation for the existing "enable_pure_p2p_tip"
entry.

In `@public/translations/vi/default.json`:
- Line 377: Replace the English value for the translation key
"enable_pure_p2p_tip" with a Vietnamese translation so the locale isn't mixed;
update the value for "enable_pure_p2p_tip" (in
public/translations/vi/default.json) to a Vietnamese string such as "Sử dụng nút
P2P trên trình duyệt thay vì chế độ tương thích gateway." to match the rest of
the locale.

In `@public/translations/zh/default.json`:
- Line 377: The translation for the key "enable_pure_p2p_tip" is still in
English; replace the English value with a Chinese localized string while keeping
the key intact (e.g., "使用浏览器 P2P 节点,而不是网关兼容模式。") so the UI language is
consistent in this section.

In `@src/components/comment-media/comment-media.module.css`:
- Around line 194-204: The .mediaLoadFailureStatus rule uses the deprecated clip
property; replace the clip: rect(0 0 0 0); declaration in the
.mediaLoadFailureStatus CSS rule with a modern clip-path equivalent (e.g.,
clip-path: inset(0 0 0 0);) and include a vendor-prefixed fallback
(-webkit-clip-path) to preserve behavior across browsers while keeping the rest
of the visually-hidden declarations intact.

In `@src/components/comment-media/comment-media.tsx`:
- Around line 388-391: handleLoad currently always calls
onMediaLoadFailureChange(undefined) even when failedUrl isn't cleared, causing
parent/component desync; update handleLoad so it uses the functional updater
result to determine if failedUrl was actually changed (i.e., check whether
currentFailedUrl === url) and only invoke onMediaLoadFailureChange(undefined)
when you actually clear the failedUrl; reference the handleLoad function,
setFailedUrl updater, failedUrl state, onMediaLoadFailureChange callback, and
url prop when implementing this conditional notification.

In `@src/hooks/use-prune-hidden-catalog-threads.ts`:
- Around line 125-149: The effect currently requeues failed unblockCid calls
immediately because pendingPruneCidsRef.current.delete(cid) runs in .finally,
causing tight retry loops when accountsActions.unblockCid(cid) keeps rejecting;
change the logic in useEffect to track per-CID failure attempts (e.g.,
failureCountsRef keyed by cid) and only remove cid from pendingPruneCidsRef on
success or after a short cooldown/after N failures, incrementing
failureCountsRef[cid] on rejection and skipping retries when count >= N; also
reset failureCountsRef when activeAccountId or communityAddress (or whatever
drives removedHiddenThreadCids) changes so retries resume for new contexts.
Ensure you update the handlers around accountsActions.unblockCid, the .catch and
.finally behavior, and the dependency list (useEffect) references for resets.

---

Nitpick comments:
In `@src/hooks/use-hidden-catalog-threads.ts`:
- Around line 167-183: The default parameter candidateComments = [] in
useHiddenCatalogThreads creates a new array each render and forces downstream
memos to recompute; fix by introducing a module-level stable empty array (e.g.,
STABLE_EMPTY_CANDIDATE_COMMENTS: Comment[] = []) and use that stable constant as
the default for candidateComments (or fall back inside the function) so
candidateCommentList's useMemo (which depends on candidateComments and computes
mergeCommentsByCid([...])) sees a stable reference and won't retrigger
unnecessarily.

In `@src/hooks/use-hide.ts`:
- Around line 68-86: The unhide function currently removes the optimistic
in-memory entry via forgetHiddenComment(cid) before calling
accountsStore.getState().accountsActions.unblockCid and does not restore it on
failure; update unhide to mirror hide's rollback: after catching errors from
unblockCid, check if getCurrentAccountHiddenState(cid) still indicates hidden
and, if so, call rememberHiddenComment(cid) to re-create the in-memory comment
(only when shouldLogHideActionError or the account state requires it), ensuring
the optimistic removal is reverted on unblock failures; reference unhide,
forgetHiddenComment, rememberHiddenComment,
accountsStore.getState().accountsActions.unblockCid, shouldLogHideActionError,
and getCurrentAccountHiddenState when making the change.

In `@src/hooks/use-prune-hidden-catalog-threads.ts`:
- Line 89: The selector currently subscribes to the entire communitiesPages map
via communitiesPagesStore((state) => state.communitiesPages); change it so the
selector only returns the entry for the current community (use communityAddress
inside the selector) or directly derive the specific value needed (e.g.
community.posts?.pageCids) so the hook (usePruneHiddenCatalogThreads or the hook
body) only re-runs when that community's pages change; update the selector call
to access state.communitiesPages[communityAddress] or return the memoized
pageCids for that community instead of the whole map.

In `@src/views/catalog/catalog.tsx`:
- Around line 397-409: The effect in useEffect controlling auto-hide of the
hidden-threads panel is non-obvious; add a concise inline comment above this
block explaining the state machine: describe that when
requestedShowHiddenThreads is false we reset hadVisibleHiddenThreadsRef, when
hiddenThreadsCount > 0 we mark hadVisibleHiddenThreadsRef.current = true, and
when either hadVisibleHiddenThreadsRef is true (threads were visible but then
disappeared) or loading finished with zero results
(!isLoadingHiddenCatalogThreads) we clear the shown scope via
setShownHiddenThreadsScopeKey(null); reference the variables useEffect,
requestedShowHiddenThreads, hadVisibleHiddenThreadsRef, hiddenThreadsCount,
isLoadingHiddenCatalogThreads, and setShownHiddenThreadsScopeKey in the comment
so future maintainers understand the two distinct cases handled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02d8fc07-3659-48a2-a861-5c37857cff7b

📥 Commits

Reviewing files that changed from the base of the PR and between 486ef39 and 6c68732.

📒 Files selected for processing (67)
  • electron/main.js
  • electron/start-pkc-rpc-core.js
  • electron/start-pkc-rpc-core.test.js
  • electron/start-pkc-rpc.js
  • electron/user-agent.js
  • electron/user-agent.test.js
  • public/translations/ar/default.json
  • public/translations/bn/default.json
  • public/translations/cs/default.json
  • public/translations/da/default.json
  • public/translations/de/default.json
  • public/translations/el/default.json
  • public/translations/en/default.json
  • public/translations/es/default.json
  • public/translations/fa/default.json
  • public/translations/fi/default.json
  • public/translations/fil/default.json
  • public/translations/fr/default.json
  • public/translations/he/default.json
  • public/translations/hi/default.json
  • public/translations/hu/default.json
  • public/translations/id/default.json
  • public/translations/it/default.json
  • public/translations/ja/default.json
  • public/translations/ko/default.json
  • public/translations/mr/default.json
  • public/translations/nl/default.json
  • public/translations/no/default.json
  • public/translations/pl/default.json
  • public/translations/pt/default.json
  • public/translations/ro/default.json
  • public/translations/ru/default.json
  • public/translations/sq/default.json
  • public/translations/sv/default.json
  • public/translations/te/default.json
  • public/translations/th/default.json
  • public/translations/tr/default.json
  • public/translations/uk/default.json
  • public/translations/ur/default.json
  • public/translations/vi/default.json
  • public/translations/zh/default.json
  • src/__tests__/app.test.tsx
  • src/app.tsx
  • src/components/board-buttons/__tests__/board-buttons.test.tsx
  • src/components/board-buttons/board-buttons.module.css
  • src/components/board-buttons/board-buttons.tsx
  • src/components/catalog-row/__tests__/catalog-row.test.tsx
  • src/components/catalog-row/catalog-row.tsx
  • src/components/comment-content/__tests__/comment-content.test.tsx
  • src/components/comment-content/comment-content.tsx
  • src/components/comment-media/__tests__/comment-media.test.tsx
  • src/components/comment-media/comment-media.module.css
  • src/components/comment-media/comment-media.tsx
  • src/components/comment-media/index.ts
  • src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx
  • src/components/post-mobile/post-menu-mobile/post-menu-mobile.tsx
  • src/components/post-mobile/post-mobile.tsx
  • src/hooks/__tests__/use-hidden-catalog-threads.test.tsx
  • src/hooks/__tests__/use-hide.test.tsx
  • src/hooks/__tests__/use-prune-hidden-catalog-threads.test.tsx
  • src/hooks/use-hidden-catalog-threads.ts
  • src/hooks/use-hide.ts
  • src/hooks/use-prune-hidden-catalog-threads.ts
  • src/lib/utils/post-menu-props.ts
  • src/stores/use-hidden-catalog-threads-store.ts
  • src/views/catalog/__tests__/catalog.test.tsx
  • src/views/catalog/catalog.tsx

pkcWebSocketServer.on('error', (e) => logger.log('pkc rpc error', e));

logger.log(`pkc rpc: listening on ws://localhost:${port} (local connections only)`);
logger.log(`pkc rpc: listening on ws://localhost:${port}/${authKey} (secret auth key for remote connections)`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not log the PKC RPC auth key in plaintext.

Line 6 includes the full authKey in logs, which leaks a live secret and weakens access control if logs are exposed.

🔒 Proposed fix
-  logger.log(`pkc rpc: listening on ws://localhost:${port}/${authKey} (secret auth key for remote connections)`);
+  logger.log(`pkc rpc: listening on ws://localhost:${port}/[redacted] (secret auth key for remote connections)`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.log(`pkc rpc: listening on ws://localhost:${port}/${authKey} (secret auth key for remote connections)`);
logger.log(`pkc rpc: listening on ws://localhost:${port}/[redacted] (secret auth key for remote connections)`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/start-pkc-rpc-core.js` at line 6, The log currently prints the full
secret authKey (logger.log in start-pkc-rpc-core.js), which leaks credentials;
change the logging in the PKC RPC startup to avoid printing authKey in
plaintext—log only non-sensitive info such as the port and a redacted or hashed
representation of authKey (e.g., mask all but last 4 chars or log
"authKey=REDACTED"/its SHA256) while keeping the existing message context so
callers can still see the service is listening without exposing the secret.

"p2p_stats": "P2P stats",
"enable_pure_p2p": "Enable pure P2P",
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize this tooltip in Arabic

Line 377 is still English (Use a browser P2P node instead of gateway compatibility mode.), so Arabic users will see mixed-language UI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/translations/ar/default.json` at line 377, Replace the English string
for the translation key "enable_pure_p2p_tip" with an Arabic translation so the
UI is fully localized for Arabic users; update the value of
"enable_pure_p2p_tip" from "Use a browser P2P node instead of gateway
compatibility mode." to an appropriate Arabic text (for example: "استخدم عقدة
P2P في المتصفح بدلاً من وضع التوافق مع البوابة.") ensuring the JSON string
remains valid.

"pass_question_governance": "Will 5chan Pass control directory governance?",
"pass_answer_governance": "No. The current direction is that 5chan Pass holders can participate in directory voting, but final governance mechanics should include BSO-holder alignment and safeguards instead of pass-only final control.",
"app_is_up_to_date": "you're on latest"
"app_is_up_to_date": "you're on latest",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten English copy for update status

Line 383 (you're on latest) reads ungrammatical; consider “You're on the latest version.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/translations/en/default.json` at line 383, Replace the ungrammatical
value for the "app_is_up_to_date" key: change the string "you're on latest" to a
properly capitalized and punctuated phrase such as "You're on the latest
version." in the JSON so the translation reads correctly.

"p2p_stats": "P2P stats",
"enable_pure_p2p": "Enable pure P2P",
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize enable_pure_p2p_tip for this locale.

Line 377 is still English, which causes mixed-language UI in Romanian. Please translate this key here and in the same changed key across public/translations/ru/default.json, public/translations/sq/default.json, public/translations/sv/default.json, public/translations/te/default.json, public/translations/th/default.json, public/translations/tr/default.json, and public/translations/uk/default.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/translations/ro/default.json` at line 377, The translation key
"enable_pure_p2p_tip" is still in English in the Romanian locale; replace the
English value with an appropriate Romanian translation for that key in
public/translations/ro/default.json and apply the corresponding translated text
to the same key ("enable_pure_p2p_tip") in the other locales mentioned (ru, sq,
sv, te, th, tr, uk) so the UI is consistent across languages; ensure you only
change the value string (keep the JSON key intact and maintain valid JSON
quoting/commas).

"p2p_stats": "P2P stats",
"enable_pure_p2p": "Enable pure P2P",
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize enable_pure_p2p_tip for Urdu UI.

Line 377 is still English, which causes mixed-language UX in the Urdu locale. Please provide an Urdu translation for this key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/translations/ur/default.json` at line 377, The key
"enable_pure_p2p_tip" currently has an English string; replace its value with an
Urdu translation so the Urdu locale is consistent — update the JSON value for
"enable_pure_p2p_tip" to an appropriate Urdu phrase such as "گیٹ وے کمپٹیبیلٹی
موڈ کی بجائے براؤزر P2P نوڈ استعمال کریں۔" ensuring proper JSON quoting and
punctuation for the existing "enable_pure_p2p_tip" entry.

"p2p_stats": "P2P stats",
"enable_pure_p2p": "Enable pure P2P",
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate enable_pure_p2p_tip into Vietnamese.

Line 377 is still in English, so this locale will show mixed-language copy in settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/translations/vi/default.json` at line 377, Replace the English value
for the translation key "enable_pure_p2p_tip" with a Vietnamese translation so
the locale isn't mixed; update the value for "enable_pure_p2p_tip" (in
public/translations/vi/default.json) to a Vietnamese string such as "Sử dụng nút
P2P trên trình duyệt thay vì chế độ tương thích gateway." to match the rest of
the locale.

"p2p_stats": "P2P stats",
"enable_pure_p2p": "Enable pure P2P",
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize enable_pure_p2p_tip for Chinese users.

Line 377 is English while the rest of this section is localized, which introduces inconsistent UI language.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/translations/zh/default.json` at line 377, The translation for the key
"enable_pure_p2p_tip" is still in English; replace the English value with a
Chinese localized string while keeping the key intact (e.g., "使用浏览器 P2P
节点,而不是网关兼容模式。") so the UI language is consistent in this section.

Comment on lines +194 to +204
.mediaLoadFailureStatus {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace deprecated clip property with modern clip-path.

The clip property at line 201 is deprecated. Modern CSS uses clip-path instead for clipping regions.

♻️ Proposed fix
 .mediaLoadFailureStatus {
   position: absolute;
   width: 1px;
   height: 1px;
   padding: 0;
   margin: -1px;
   overflow: hidden;
-  clip: rect(0 0 0 0);
+  clip-path: inset(50%);
   white-space: nowrap;
   border: 0;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.mediaLoadFailureStatus {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.mediaLoadFailureStatus {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
🧰 Tools
🪛 Stylelint (17.11.0)

[error] 201-201: Deprecated property "clip" (property-no-deprecated)

(property-no-deprecated)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/comment-media/comment-media.module.css` around lines 194 -
204, The .mediaLoadFailureStatus rule uses the deprecated clip property; replace
the clip: rect(0 0 0 0); declaration in the .mediaLoadFailureStatus CSS rule
with a modern clip-path equivalent (e.g., clip-path: inset(0 0 0 0);) and
include a vendor-prefixed fallback (-webkit-clip-path) to preserve behavior
across browsers while keeping the rest of the visually-hidden declarations
intact.

Comment on lines +388 to +391
const handleLoad = () => {
setFailedUrl((currentFailedUrl) => (currentFailedUrl === url ? undefined : currentFailedUrl));
onMediaLoadFailureChange?.(undefined);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

State synchronization bug in handleLoad.

The callback onMediaLoadFailureChange?.(undefined) is called unconditionally, but failedUrl is only cleared when currentFailedUrl === url. When the media URL changes while a previous URL is still marked as failed, the component state and parent state desynchronize.

Scenario:

  1. Image B fails → failedUrl = "B", parent receives "B"
  2. URL changes to C, C loads successfully
  3. setFailedUrl sees currentFailedUrl = "B" and url = "C""B" !== "C" → keeps failedUrl = "B"
  4. Callback fires with undefined → parent clears its state
  5. Result: component has failedUrl = "B", parent has undefinedinconsistent state
🐛 Proposed fix: only notify parent when state actually changes
 const handleLoad = () => {
-  setFailedUrl((currentFailedUrl) => (currentFailedUrl === url ? undefined : currentFailedUrl));
-  onMediaLoadFailureChange?.(undefined);
+  setFailedUrl((currentFailedUrl) => {
+    if (currentFailedUrl === url) {
+      onMediaLoadFailureChange?.(undefined);
+      return undefined;
+    }
+    return currentFailedUrl;
+  });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleLoad = () => {
setFailedUrl((currentFailedUrl) => (currentFailedUrl === url ? undefined : currentFailedUrl));
onMediaLoadFailureChange?.(undefined);
};
const handleLoad = () => {
setFailedUrl((currentFailedUrl) => {
if (currentFailedUrl === url) {
onMediaLoadFailureChange?.(undefined);
return undefined;
}
return currentFailedUrl;
});
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/comment-media/comment-media.tsx` around lines 388 - 391,
handleLoad currently always calls onMediaLoadFailureChange(undefined) even when
failedUrl isn't cleared, causing parent/component desync; update handleLoad so
it uses the functional updater result to determine if failedUrl was actually
changed (i.e., check whether currentFailedUrl === url) and only invoke
onMediaLoadFailureChange(undefined) when you actually clear the failedUrl;
reference the handleLoad function, setFailedUrl updater, failedUrl state,
onMediaLoadFailureChange callback, and url prop when implementing this
conditional notification.

Comment on lines +125 to +149
useEffect(() => {
if (!enabled || removedHiddenThreadCids.length === 0) {
return;
}

for (const cid of removedHiddenThreadCids) {
if (pendingPruneCidsRef.current.has(cid)) {
continue;
}

pendingPruneCidsRef.current.add(cid);
void accountsStore
.getState()
.accountsActions.unblockCid(cid)
.catch((error: unknown) => {
const { accounts, activeAccountId } = accountsStore.getState();
if (activeAccountId && accounts?.[activeAccountId]?.blockedCids?.[cid]) {
console.error('Failed to remove stale hidden thread from account', error);
}
})
.finally(() => {
pendingPruneCidsRef.current.delete(cid);
});
}
}, [enabled, removedHiddenThreadCids]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Persistent unblockCid failures can be retried in a tight loop.

pendingPruneCidsRef only prevents concurrent duplicate calls — once .finally (Line 145-147) removes the CID, the effect is free to re-issue unblockCid for the same CID on the very next render. The effect's deps include removedHiddenThreadCids, which is derived from rawBoardCatalogState (and thus communitiesPages, which updates frequently). If accountsActions.unblockCid(cid) rejects persistently (server error, transient outage), the failed CID stays in blockedCids, stays archived-not + in-scope + not-in-rootThreadCids, and gets re-attempted on every catalog page update — silently, since the error is only logged when accounts[activeAccountId].blockedCids[cid] is truthy (which it always is on persistent failure).

Consider one of:

  • Track per-CID failure counts in a ref and skip after N consecutive failures (with a reset on communityAddress/accountId change).
  • Move pending-set removal out of .finally for the error path and require an explicit retry trigger.
  • Add a short cooldown before the same CID is retried.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/use-prune-hidden-catalog-threads.ts` around lines 125 - 149, The
effect currently requeues failed unblockCid calls immediately because
pendingPruneCidsRef.current.delete(cid) runs in .finally, causing tight retry
loops when accountsActions.unblockCid(cid) keeps rejecting; change the logic in
useEffect to track per-CID failure attempts (e.g., failureCountsRef keyed by
cid) and only remove cid from pendingPruneCidsRef on success or after a short
cooldown/after N failures, incrementing failureCountsRef[cid] on rejection and
skipping retries when count >= N; also reset failureCountsRef when
activeAccountId or communityAddress (or whatever drives removedHiddenThreadCids)
changes so retries resume for new contexts. Ensure you update the handlers
around accountsActions.unblockCid, the .catch and .finally behavior, and the
dependency list (useEffect) references for resets.

@tomcasaburi
Copy link
Copy Markdown
Member Author

Review triage before merge: the branch is already up to date with current master, and its unique diff is only electron/main.js, electron/user-agent.js, and electron/user-agent.test.js. Cursor Bugbot's PKC RPC options comment is declined: the installed @pkcprotocol/pkc-js/rpc schema and implementation expect pkcOptions. CodeRabbit reviewed the older 486ef39..6c68732 range and posted comments on files already present on master, so those are pre-existing/follow-up items and do not block this Electron user-agent fix. Verified locally with yarn build, yarn lint, yarn type-check, and yarn test; GitHub checks are green.

@tomcasaburi tomcasaburi merged commit 8486401 into master May 14, 2026
11 checks passed
@tomcasaburi tomcasaburi deleted the codex/fix/linux-turnstile-user-agent branch May 14, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant