Skip to content

Add tor routing, toggle, icons#1205

Open
alltheseas wants to merge 21 commits intodamus-io:masterfrom
alltheseas:feature/tor-routing
Open

Add tor routing, toggle, icons#1205
alltheseas wants to merge 21 commits intodamus-io:masterfrom
alltheseas:feature/tor-routing

Conversation

@alltheseas
Copy link
Copy Markdown
Contributor

@alltheseas alltheseas commented Nov 11, 2025

🧅 Experimental Tor Branch 🧅

Built with rust Arti
https://github.com/zydou/arti

image image

Puffin profile + top right icon showing Tor is disconnected

image

Puffin profile + top right icon showing Tor is connected

image

OS limitations

Linus, macOS + Windows run the full TorManager (crates/notedeck/src/tor.rs:1-
259), so the desktop builds spawn the bundled Arti runtime in a background
thread, expose the SOCKS proxy on 127.0.0.1:, and toggle the VPN-style
badge plus relay transport automatically. No platform gating is in place for
those OSes.

  • The only platform-specific blocker today is Android (plus wasm, which shares
    that stub module). On macOS/Windows/Linux the Tor toggle, status polling,
    and WebSocket transport switching all operate the same way.

Summary

  • Add a native Tor manager (Arti runtime + SOCKS proxy) and persist its state
    in Settings → Network, wiring the toggle through SettingsAction so enabling/
    disabling Tor reconfigures relay sockets without restarting the app.
  • Teach RelayPool/Relay to carry customized ewebsock::Options, and vendor a
    patched ewebsock that supports SOCKS5 handshakes—enabling all relay traffic
    to transparently route through Tor when the toggle is on.
  • Surface Tor routing state in the chrome UI: Always display a Tor onion badge
    in the top-right corner with color/iconography for “connected,” “starting,”
    “disabled,” and “error,” mirroring the official Tor palette to make the
    state immediately visible.

Testing

  • cargo check
  • Manually toggled Tor in Settings → Network (watching badge transition
    through Connected → Disabled and verifying relays flip between direct &
    SOCKS options).

Summary by CodeRabbit

  • New Features

    • Desktop & Android Tor integration with background Tor runtime and local SOCKS proxy.
    • UI Tor indicator with real-time status and a settings toggle to enable/disable Tor.
    • Optional SOCKS proxy routing for all HTTP image and network fetches.
  • Chores

    • Android native Tor build and packaging support added.
    • Build/workspace and packaging updates to support native libs and platform-specific dependencies.
  • Style

    • VCS ignore and attributes updated.

✏️ Tip: You can customize this high-level summary in your review settings.

@alltheseas
Copy link
Copy Markdown
Contributor Author

Review of code:

Risk-Ordered Findings

  • Tor toggle desync (settings stays “on” even when runtime failed): high
    severity × high probability—users think they’re protected while relays run
    direct; needs automatic revert or retriable state. crates/notedeck_columns/
    src/ui/settings.rs:514-555
  • No automatic retry/backoff for Tor bootstrap: high severity × medium
    probability—transient network issues leave Tor permanently failed until
    manual toggle; implement bounded retry with jitter. crates/notedeck/src/
    tor.rs:120-187
  • Fixed SOCKS port with no conflict detection: medium-high severity × medium
    probability—collides with Tor Browser/system Tor leading to silent failure;
    probe/auto-select open port or surface clearer error. crates/notedeck/src/
    tor.rs:20, 139-153
  • Transport switch leaves existing relay sockets direct: medium severity ×
    medium probability—some relays stay outside Tor after enabling; pool should
    reconnect or warn. crates/notedeck/src/app.rs:433-451
  • Missing circuit isolation: medium severity × medium probability—shared
    circuits allow cross-relay correlation; enable Arti isolation per
    destination. crates/notedeck/src/tor.rs:208-233
  • Lack of bootstrap progress feedback: medium severity × medium probability—
    users assume hang/failure during long connect; expose Arti bootstrap events
    to UI. crates/notedeck/src/tor.rs:200-233, crates/notedeck_chrome/src/
    chrome.rs:900-968
  • Error copy surfaces internal strings: low-medium severity × high probability
    —raw Arti errors leak jargon and confuse users; map to friendly text
    while logging details. crates/notedeck/src/tor.rs:150-180, crates/
    notedeck_columns/src/ui/settings.rs:524-555
  • Badge/action affordance gaps (no click for info/retry): low severity ×
    medium probability—users can’t recover from failure via badge though they
    notice it. crates/notedeck_chrome/src/chrome.rs:901-968
  • Android “unsupported” lacks guidance: low severity × medium probability
    —status just says unsupported without pointing to alternatives (e.g.,
    Orbot). crates/notedeck/src/tor.rs:266-307, crates/notedeck_columns/src/ui/
    settings.rs:550-555
  • Thread shutdown watchdog: low severity × low probability—handle.join() waits
    forever only if runtime ignores shutdown; unlikely but could add timeout.
    crates/notedeck/src/tor.rs:44-71

(Note: dependency mismatch/derive-deftly conflict is already fixed by pinning
Arti commit; DNS leak, SOCKS auth length, and badge visibility critiques were
incorrect per current code.)

@alltheseas alltheseas mentioned this pull request Nov 12, 2025
@alltheseas
Copy link
Copy Markdown
Contributor Author

@coderabbitai review please

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 15, 2026

@alltheseas: I'll review the changes in this PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 15, 2026

Warning

Rate limit exceeded

@alltheseas has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 15 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8afb8 and 1449111.

📒 Files selected for processing (3)
  • crates/notedeck/src/app.rs
  • crates/notedeck/src/media/static_imgs.rs
  • crates/notedeck/src/tor.rs
📝 Walkthrough

Walkthrough

Adds cross-platform Tor support: desktop runtime-managed Tor client with SOCKS proxy, Android JNI/native Arti integration, runtime-configurable WebSocket transport via SOCKS, HTTP fetching over SOCKS, UI settings to toggle Tor, and build tooling to compile Arti for Android.

Changes

Cohort / File(s) Summary
Workspace & Manifests
.cargo/config.toml, Cargo.toml, crates/notedeck/Cargo.toml, tools/arti-build/Cargo.toml
Add aarch64 SQLite bundling flags; add arti/tor-related dependencies and TLS/HTTP deps with explicit features; introduce arti-android cdylib manifest.
Build Tools & Scripts
Makefile, tools/arti-build/build-arti.sh, tools/arti-build/ARTI_VERSION
New arti build targets and scripts; ARTI_VERSION file added; Makefile targets for arti builds and android-tor workflow.
Core App & Context
crates/notedeck/src/app.rs, crates/notedeck/src/context.rs, crates/notedeck/src/lib.rs
Add TorManager field and ws_transport wiring; new update_relay_transport method; expose TorManager/TorStatus and platform-gated crypto install.
Tor Implementation (Desktop/Android stubs)
crates/notedeck/src/tor.rs
New TorManager/TorStatus API and implementations: desktop runtime that spawns background Tor thread, readiness signaling, start/stop/poll/socks proxy management; wasm/android fallbacks and Android JNI path.
Android JNI & Native Arti
tools/arti-build/src/lib.rs, crates/notedeck/src/jni_cache.rs
JNI wrapper for Arti: initialize/startSocksProxy/stop/getSocksPort/isInitialized/getVersion/setLogCallback; JNI ClassLoader cache for cross-thread class loading on Android.
Android Java Integration
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java, .../tor/*.java
New TorProvider interface, NativeTorProvider (native Arti wrapper), StubTorProvider fallback, TorManager singleton, TorLogCallback, and MainActivity JNI init call.
WebSocket Relay & Pool
crates/enostr/src/relay/mod.rs, crates/enostr/src/relay/pool.rs
Relay now stores Options; RelayPool keeps ws_options and propagates them; new configure_transport to update transport and reconnect relays (cfg-gated).
Networking & Image Caches
crates/notedeck/src/media/network.rs, crates/notedeck/src/media/static_imgs.rs, crates/notedeck/src/media/gif.rs, crates/notedeck/src/imgcache.rs
Add HttpConfig (socks_proxy), http_fetch dispatcher, http_req_via_socks (desktop) with redirects/chunked handling, and thread-through of http_config into static/animated image caches and texture caches.
Settings & Persistence
crates/notedeck/src/persist/settings_handler.rs, crates/notedeck_columns/src/ui/settings.rs, crates/notedeck_columns/src/nav.rs
Add use_tor setting with getter/setter; SettingsAction::ToggleTor; SettingsView UI extended to show Tor toggle and status; handlers now accept tor context.
UI Indicator
crates/notedeck_chrome/src/chrome.rs
Add TorStatus-driven onion badge rendering, styles, tooltips, and integrate TorStatus into chrome rendering.
Android Gradle & Misc
crates/notedeck_chrome/android/app/build.gradle, .gitignore, .gitattributes
minSdk→21, ndk abiFilters and jniLibs sourceSets, release minify disabled; gitignore updates and beads merge driver in .gitattributes.

Sequence Diagram(s)

sequenceDiagram
    participant App as Notedeck App
    participant TorMgr as TorManager
    participant Runtime as TorRuntime
    participant RelayPool as RelayPool
    participant Network as Network Layer

    App->>TorMgr: new(data_path) / set_enabled(true)
    TorMgr->>Runtime: spawn background thread (run_tor_runtime)
    Runtime->>Runtime: build config, bootstrap Tor, start SOCKS proxy
    Runtime-->>TorMgr: ReadyState::Ready(port)
    App->>TorMgr: poll()
    TorMgr-->>App: status = Running(port)

    App->>App: update_relay_transport()
    App->>RelayPool: configure_transport(Socks(proxy), wakeup)
    RelayPool->>RelayPool: update ws_options
    RelayPool->>RelayPool: reconnect relays using ws_options

    App->>Network: http_fetch(url, socks_proxy)
    Network->>Network: http_req_via_socks(url, socks_proxy)
    Network->>Runtime: connect via SOCKS5
    Runtime->>Network: forward data
    Network-->>App: HyperHttpResponse
Loading
sequenceDiagram
    participant User as User
    participant UI as Settings UI
    participant App as Notedeck App
    participant TorMgr as TorManager

    User->>UI: toggle Tor on
    UI->>App: emit SettingsAction::ToggleTor(true)
    App->>TorMgr: set_enabled(true)
    TorMgr->>Runtime: spawn/start Tor
    Runtime-->>TorMgr: ReadyState::Ready(port)
    App->>App: update_relay_transport() -> configure RelayPool to use SOCKS
    UI-->>User: show Tor status Running
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🥕 I dug a tunnel, quiet and small,
Where socks and onions answer the call,
JNI whispers, Arti hums low,
Relays reconnect, proxies glow—
Hop, hop, the rabbit guards the crawl.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add tor routing, toggle, icons' directly describes the main changes: Tor integration with UI toggle and indicator icons.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

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

🤖 Fix all issues with AI agents
In `@Cargo.toml`:
- Line 127: The Cargo.toml currently uses a local path dependency for ewebsock
("ewebsock = { path = \"vendor/ewebsock\" }"); replace this with a git-based
dependency that points to our fork that includes the SOCKS5 changes by updating
the ewebsock entry to reference the fork's git URL (and optional branch or
revision) so the project no longer depends on vendored source; ensure the
dependency key remains "ewebsock" and include the fork's repo URL and
branch/commit in the entry (and remove the path attribute).

In `@crates/notedeck_columns/src/ui/settings.rs`:
- Around line 545-550: The TorStatus::Failed(err) branch currently exposes the
raw internal error to users; instead, log the detailed error (e.g., using
tracing::debug or tracing::error with err) and return a generic, localized
user-facing label via the tr! macro (using self.note_context.i18n) such as "Tor
status: Connection failed" or a mapped, friendly string; ensure you keep tracing
of err for diagnostics but do not interpolate err into the displayed
translation.

In `@crates/notedeck/src/tor.rs`:
- Around line 38-47: The stop method on TorHandle currently calls handle.join()
which can block indefinitely if the Tor runtime ignores the shutdown signal;
modify TorHandle::stop to send the shutdown via self.shutdown.take() as is, then
poll the thread (self.thread.take() / handle) for a bounded timeout (e.g., 5s)
using handle.is_finished() in a loop with short sleeps, logging a warning if the
timeout elapses, and only call handle.join() if is_finished() is true to avoid
blocking forever; reference TorHandle, stop, shutdown, thread, join, and
is_finished when implementing this change.
- Around line 218-232: Ready signal is sent too early: move sending
ReadyState::Ok (ready_tx_clone.send) to after the proxy is confirmed listening
rather than immediately after tor.create_bootstrapped(); update proxy::run_proxy
(or its call site) so it reports successful bind before
returning/continuing—e.g., add a bind-complete oneshot or change run_proxy to
return a Result that resolves when the SOCKS port is bound, then send
ReadyState::Ok only after that confirmation (use the existing listen/Listen and
proxy_future/shutdown_rx flow to await bind success before sending).
- Line 19: Add docstrings to the private helper functions build_client_config()
and path_to_string() describing their purpose, inputs, outputs, and any
important behavior (e.g., build_client_config() constructs the tor client
configuration including the default SOCKS port and error handling;
path_to_string() converts a Path/PathBuf to a String and how it handles
invalid/unicode paths). Update the function comments immediately above each
function with concise Rust-style /// doc comments that follow existing project
tone and mention any special cases (like the hardcoded 9150 port and that
binding failures are reported via TorStatus::Failed).

In `@vendor/ewebsock/.cargo-ok`:
- Line 1: The repository currently vendors ewebsock (presence of
vendor/ewebsock/.cargo-ok) without justification; either stop vendoring and
reference a maintained fork in Cargo.toml (add the fork crate reference and
remove the vendored directory), or if vendoring is unavoidable, add a clear
justification comment/README next to vendor/ewebsock/.cargo-ok explaining why a
fork/published crate cannot be used (e.g., upstream unresponsive, required
unpublished patches, licensing constraints), list the specific changes that
required vendoring, and document maintenance expectations and contact for future
updates; update Cargo metadata accordingly so maintainers can see the rationale.

In `@vendor/ewebsock/Cargo.toml.orig`:
- Around line 1-75: This patch vendors the ewebsock crate (package "ewebsock")
which violates the guideline; push your SOCKS5 changes to a public/private fork
of the ewebsock repository and update the Cargo.toml dependency to reference
that fork via a git dependency (e.g., set ewebsock in Cargo.toml to use git =
"https://.../your-fork.git" and optionally rev = "..." or tag) instead of
keeping the copy under vendor/, and if vendoring is absolutely unavoidable add a
short justification and link to the upstream issue/PR in the repo docs or a
VENDORING.md explaining why a fork was not possible.

In `@vendor/ewebsock/src/native_tungstenite_tokio.rs`:
- Around line 80-83: The on_event callback return value (used at the
WsEvent::Opened call and the reader closure) currently ignores
ControlFlow::Break and merely logs a warning; update the logic so
ControlFlow::Break is honored: have the on_event(...) result be checked and, if
it indicates Break, break out of the reader loop / return from the async reader
task immediately instead of continuing, or change the callback signature to
return Result<(), E> (or bool) and propagate that result so callers can stop
processing; adjust handling around the on_event(WsEvent::Opened) call and the
reader async closure so the Break/Err path causes an immediate exit rather than
continuing.
- Around line 159-172: The test test_connect_tokio attempts to open a real
WebSocket to example.com which has no WebSocket endpoint and will fail; either
convert the test to use a local/mock WebSocket server (spawn a tokio-based
echo/mock server and call crate::connect against its address) or mark the test
as an ignored/integration test by adding the appropriate test attribute (e.g.,
#[ignore]) and a comment referencing crate::connect and test_connect_tokio so CI
won't run a failing external connection; implement whichever approach is
appropriate for your test suite.

In `@vendor/ewebsock/src/native_tungstenite.rs`:
- Around line 200-223: The code currently panics on WsMessage::Unknown in the
send loop; replace the panic with a non-panicking error-handling path: when
matching WsMessage::Unknown(_) in the match inside the loop that handles
rx.try_recv(), log a warning or construct and return an Err with a clear message
(e.g., "cannot send WsMessage::Unknown") instead of panic!, ensuring you follow
the surrounding function's Result error type (the same style used for socket
send errors); keep existing cleanup behavior (socket.close/flush) if you return
an Err so resources are cleaned up consistently.

In `@vendor/ewebsock/src/web.rs`:
- Around line 32-47: The send() method in web.rs currently panics for
WsMessage::Ping, WsMessage::Pong and other unknown variants; update the match in
send() (inside the send method) to handle WsMessage::Ping and WsMessage::Pong
instead of falling through to the panic: add explicit arms for WsMessage::Ping
and WsMessage::Pong that either translate them to an appropriate send call on
socket (e.g., send a small binary/frame or text payload if your websocket
backend requires) or safely no-op/log the event with log::debug!/log::warn! so
they don't panic, and add a final arm for WsMessage::Unknown that logs and
returns an error instead of panicking; reference the send function and the
WsMessage enum variants when making the change.
🧹 Nitpick comments (18)
vendor/ewebsock/Cargo.toml (1)

90-96: Version ranges may affect build reproducibility.

The tungstenite dependencies use version ranges (>=0.23, <=0.24), which could lead to different builds depending on when dependencies are resolved.

Since this is a vendored crate, consider pinning to exact versions for more predictable builds, or document which specific version was tested.

crates/notedeck_chrome/src/chrome.rs (2)

919-942: Tor indicator implementation follows egui idioms correctly.

The Area positioning, layering with Order::Tooltip, and non-blocking rendering are all appropriate. The subtle background blending provides good visual integration.

Consider adding click affordance (as noted in PR comments) to allow users to retry on failure or access Tor settings:

💡 Optional: Add click-to-action affordance
let response = egui::Frame::new()
    // ... existing frame setup ...
    .show(area_ui, |ui| {
        // ... existing icon painting ...
    });

// Make clickable for retry/settings navigation
if response.response.clicked() {
    // Could emit an action to navigate to Settings or retry Tor
}
response.response.on_hover_text(style.tooltip);

968-979: Consider sanitizing error messages in the Failed tooltip.

The error message from err.as_str() is displayed directly in the tooltip. Per PR comments, internal Arti error strings may be cryptic or expose implementation details. Consider mapping common errors to user-friendly messages while logging the raw error for debugging.

💡 Optional: User-friendly error mapping
TorStatus::Failed(err) => {
    let user_msg = match err.as_str() {
        s if s.contains("timeout") => "Connection timed out",
        s if s.contains("network") => "Network error",
        _ => "Connection failed",
    };
    Some(TorIndicatorStyle {
        // ...
        tooltip: tr!(i18n, "Tor routing failed: {error}", ..., error = user_msg),
    })
}
crates/notedeck/Cargo.toml (1)

56-60: Target-gate Tor dependencies for clarity and to prevent potential transitive dependency issues on unsupported platforms.

The tor module already has proper conditional compilation (main implementation excluded on Android/wasm with a stub provided), however, the Arti dependencies in Cargo.toml lack explicit target gating. While Cargo correctly handles this when all consuming code is gated with #[cfg], explicitly gating these dependencies in Cargo.toml makes the intent clearer and preemptively avoids potential issues from transitive dependencies that may not support these platforms.

♻️ Suggested target-specific dependencies
[target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies]
arti = { workspace = true }
arti-client = { workspace = true }
tor-config = { workspace = true }
tor-config-path = { workspace = true }
tor-rtcompat = { workspace = true }
crates/notedeck/src/persist/settings_handler.rs (1)

32-41: Consider adding #[serde(default)] for backward compatibility.

When users upgrade from a version without use_tor, their existing settings.json won't contain this field. Without #[serde(default)] on either the field or struct, deserialization will fail (Line 130-138 falls back to defaults, but this may cause unnecessary error logging).

Consider annotating the struct or field:

Option 1: Default at struct level (recommended for future fields)
-#[derive(Serialize, Deserialize, PartialEq, Clone)]
+#[derive(Serialize, Deserialize, PartialEq, Clone, Default)]
+#[serde(default)]
 pub struct Settings {
Option 2: Default at field level
+    #[serde(default)]
     pub use_tor: bool,
crates/enostr/src/relay/mod.rs (1)

231-234: Note: update_options does not trigger reconnection.

This method updates stored options without reconnecting. Callers must explicitly call connect() afterward if they want changes to take effect immediately. This is the current design (as seen in RelayPool::configure_transport), but consider documenting this behavior.

📝 Optional: Add docstring clarifying reconnection requirement
+    /// Updates the connection options for this relay.
+    ///
+    /// Note: This does not trigger a reconnection. Call `connect()` afterward
+    /// for the new options to take effect.
     pub fn update_options(&mut self, options: Options) {
         self.options = options;
     }
vendor/ewebsock/src/native_tungstenite_tokio.rs (4)

23-27: Cloning sender and spawning per-send is inefficient.

Each send() clones the Sender and spawns a new Tokio task. This adds overhead compared to the blocking version in native_tungstenite.rs which uses tx.send(msg).ok() synchronously. Consider using try_send() for non-blocking behavior without spawning:

♻️ Suggested improvement
     pub fn send(&mut self, msg: WsMessage) {
-        if let Some(tx) = self.tx.clone() {
-            tokio::spawn(async move { tx.send(msg).await });
+        if let Some(tx) = &self.tx {
+            if let Err(err) = tx.try_send(msg) {
+                log::warn!("Failed to send WebSocket message: {err}");
+            }
         }
     }

93-93: Consider returning an error instead of panicking on WsMessage::Unknown.

Panicking in library code can be disruptive. Consider filtering out or logging Unknown messages instead:

♻️ Suggested alternative
-            WsMessage::Unknown(_) => panic!("You cannot send WsMessage::Unknown"),
+            WsMessage::Unknown(_) => {
+                log::warn!("Ignoring attempt to send WsMessage::Unknown");
+                return;
+            }

Note: This would require restructuring the stream mapping slightly.


139-139: Consider making the channel buffer size configurable.

The hardcoded buffer of 1000 messages may be excessive for typical WebSocket usage. Consider deriving this from Options or using a smaller default.


148-151: Unnecessary url.clone() - the url is already moved into the async block.

♻️ Suggested fix
     tokio::spawn(async move {
-        ws_connect_async(url.clone(), options, outgoing_messages_stream, on_event).await;
+        ws_connect_async(url, options, outgoing_messages_stream, on_event).await;
         log::debug!("WS connection finished.");
     });
vendor/ewebsock/src/web.rs (2)

69-74: Options are silently ignored in WASM implementation.

The _ignored_options parameter suggests intentional omission, but this differs from native behavior. Consider logging when non-default options are passed, or documenting this limitation in the public API.


88-88: Send bound on Rc is misleading for single-threaded WASM.

Rc<dyn Send + Fn...> has a Send bound that's meaningless in WASM's single-threaded context. This likely comes from the EventHandler type alias for API consistency with native targets. Consider using a cfg-specific type alias if this causes confusion.

crates/notedeck/src/tor.rs (3)

201-240: Consider documenting the Tor runtime lifecycle and error paths.

The function handles multiple error scenarios but lacks docstrings explaining the expected behavior. Per coding guidelines, ensure docstring coverage for added code.

📝 Suggested documentation
+    /// Launch the blocking Arti task inside the runtime and wait for shutdown.
+    ///
+    /// # Lifecycle
+    /// 1. Creates the Tor runtime and bootstraps the client
+    /// 2. Sends `ReadyState::Ok` once bootstrap completes
+    /// 3. Starts the SOCKS proxy on the specified port
+    /// 4. Runs until shutdown signal or proxy error
+    ///
+    /// # Errors
+    /// Returns an error string if runtime creation, bootstrap, or proxy fails.
+    /// Errors are also sent via `ready_tx` for the manager to handle.
     fn run_tor_runtime(

291-293: set_enabled returns Ok(()) on unsupported platforms, which may be misleading.

Returning Ok(()) when Tor is unsupported could lead callers to believe Tor was successfully enabled. Consider returning an error or checking is_supported() in callers:

♻️ Alternative approach
         pub fn set_enabled(&mut self, _enabled: bool) -> Result<(), String> {
-            Ok(())
+            Err("Tor is not supported on this platform".to_owned())
         }

Or document that callers should check is_supported() first.


267-274: TorStatus enum is duplicated across platform-specific modules.

The enum definition appears in both the main module (lines 57-63) and the fallback (lines 268-274). Consider moving it outside the inner modules to avoid duplication:

♻️ Suggested refactor
/// Overall Tor connection status used throughout the UI.
#[derive(Clone, Debug)]
pub enum TorStatus {
    Disabled,
    Starting,
    Running { socks_port: u16 },
    Failed(String),
    Unsupported,
}

#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
mod inner {
    use super::TorStatus;
    // ... rest of implementation
}

#[cfg(any(target_arch = "wasm32", target_os = "android"))]
mod inner {
    use super::TorStatus;
    // ... rest of implementation
}
crates/notedeck_columns/src/ui/settings.rs (1)

94-102: Toggle state may briefly desync if set_enabled fails.

The settings are updated before checking if Tor enablement succeeds. On failure, the setting is reverted, but the UI toggle may have already reflected the change. Consider checking support and enablement before updating settings:

♻️ Suggested ordering
             Self::ToggleTor(enabled) => {
-                settings.set_use_tor(enabled);
                 if notedeck::TorManager::is_supported() {
                     if let Err(err) = tor.set_enabled(enabled) {
                         tracing::error!("failed to toggle tor: {err}");
-                        settings.set_use_tor(!enabled);
+                    } else {
+                        settings.set_use_tor(enabled);
                     }
+                } else {
+                    settings.set_use_tor(enabled);
                 }
             }
vendor/ewebsock/src/native_tungstenite.rs (2)

483-495: Silent catch-all may skip timeout on other TLS backends.

The _ => {} arm silently ignores any MaybeTlsStream variants not explicitly handled. If native-tls feature is ever enabled, timeouts won't be set for those connections. Consider logging a warning or adding explicit handling.

Proposed improvement
         #[cfg(feature = "tls")]
         MaybeTlsStream::Rustls(s) => {
             s.get_mut()
                 .set_read_timeout(value)
                 .map_err(|err| format!("failed to set read timeout: {err}"))?;
         }
-        _ => {}
+        #[cfg(feature = "native-tls")]
+        MaybeTlsStream::NativeTls(s) => {
+            s.get_mut()
+                .set_read_timeout(value)
+                .map_err(|err| format!("failed to set read timeout: {err}"))?;
+        }
+        _ => {
+            log::warn!("Unable to set read timeout on unknown stream type");
+        }
     };

500-506: Test depends on external network; may fail in CI.

This test connects to ws://example.com, which requires network access and may fail intermittently or in sandboxed CI environments. Consider using a local mock server or marking this as an ignored integration test.

Suggestion
 #[test]
+#[ignore] // Requires network access
 fn test_connect() {
     let options = crate::Options::default();
     // see documentation for more options
     let (mut sender, _receiver) = crate::connect("ws://example.com", options).unwrap();
     sender.send(crate::WsMessage::Text("Hello!".into()));
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eac5d41 and f00bea3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • .cargo/config.toml
  • Cargo.toml
  • crates/enostr/src/relay/mod.rs
  • crates/enostr/src/relay/pool.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/app.rs
  • crates/notedeck/src/context.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/persist/settings_handler.rs
  • crates/notedeck/src/tor.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • vendor/ewebsock/.cargo-ok
  • vendor/ewebsock/.cargo_vcs_info.json
  • vendor/ewebsock/Cargo.toml
  • vendor/ewebsock/Cargo.toml.orig
  • vendor/ewebsock/README.md
  • vendor/ewebsock/src/lib.rs
  • vendor/ewebsock/src/native_tungstenite.rs
  • vendor/ewebsock/src/native_tungstenite_tokio.rs
  • vendor/ewebsock/src/tungstenite_common.rs
  • vendor/ewebsock/src/web.rs
🧰 Additional context used
📓 Path-based instructions (4)
crates/notedeck*/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

crates/notedeck*/src/**/*.rs: Use AppContext to access mutable handles to services (database, caches, relay pool, account state, localization, settings, wallet) rather than accessing global state
Never use Mutexes in UI paths; the render loop must never block. Use Rc<RefCell<>> for single-threaded interior mutability, Arc<tokio::sync::RwLock<>> for cross-thread sharing, and poll_promise::Promise for async results
Wrap async work in poll_promise::Promise, check with promise.ready() or promise.ready_mut() each frame—never block the render loop
Use tokio::sync::RwLock instead of Arc<Mutex<>> for cross-thread sharing in Notedeck
Persist state via TimedSerializer::try_save to avoid blocking the frame; batch mutations with SettingsHandler::update_batch
Wrap user-facing strings with tr! or tr_plural! macros for localization and run python3 scripts/export_source_strings.py after changing strings
Use JobPool for CPU-bound work and return results via tokio::sync::oneshot wrapped in Promises; use tokio::spawn() for network I/O and relay sync
Mark performance-critical functions with #[profiling::function] for visibility in the puffin profiler
Use tracing macros for structured logging and profiling scopes where hot paths exist
Prefer early returns and guard clauses over deeply nested conditionals; simplify control flow by exiting early instead of wrapping logic in multiple layers of if statements (Nevernesting principle)
Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference
Ensure docstring coverage for any code added or modified
Avoid Mutexes in Notedeck code; prefer poll_promise::Promise for async results, Rc<RefCell<>> for single-threaded interior mutability, or tokio::sync::RwLock when cross-thread sharing is truly necessary
Never block the render loop; use Promise::ready() for non-blocking result checks. Offload CPU-heavy work to...

Files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/persist/settings_handler.rs
  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/src/tor.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_columns/src/nav.rs
crates/notedeck*/src/lib.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use strict Rust 2021 edition; ensure edition-lints are strict and clippy disallowed_methods is denied at crate root to enforce API hygiene

Files:

  • crates/notedeck/src/lib.rs
Cargo.toml

📄 CodeRabbit inference engine (AGENTS.md)

Do not vendor code; in Cargo.toml replace the existing URL with the fork that includes the new code. If vendoring is absolutely necessary, present the case why no other options are feasible

Files:

  • Cargo.toml
crates/notedeck*/src/app.rs

📄 CodeRabbit inference engine (AGENTS.md)

Implement the App trait with update(&mut self, &mut AppContext, &mut egui::Ui) -> AppResponse to drive egui rendering and signal high-level actions

Files:

  • crates/notedeck/src/app.rs
🧠 Learnings (17)
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Prefer module-level organization over monolithic files; each feature (accounts, decks, timelines, media) should live in its own module tree

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Tests should live alongside modules (e.g., in test submodules), often using `#[tokio::test]` when async behavior is involved

Applied to files:

  • crates/notedeck/src/lib.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Ensure docstring coverage for any code added or modified

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `tokio::sync::RwLock` instead of `Arc<Mutex<>>` for cross-thread sharing in Notedeck

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/lib.rs : Use strict Rust 2021 edition; ensure edition-lints are strict and clippy `disallowed_methods` is denied at crate root to enforce API hygiene

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/Cargo.toml
  • vendor/ewebsock/Cargo.toml
  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/context.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Avoid Mutexes in Notedeck code; prefer `poll_promise::Promise` for async results, `Rc<RefCell<>>` for single-threaded interior mutability, or `tokio::sync::RwLock` when cross-thread sharing is truly necessary

Applied to files:

  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `JobPool` for CPU-bound work and return results via `tokio::sync::oneshot` wrapped in Promises; use `tokio::spawn()` for network I/O and relay sync

Applied to files:

  • crates/enostr/src/relay/pool.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Persist state via `TimedSerializer::try_save` to avoid blocking the frame; batch mutations with `SettingsHandler::update_batch`

Applied to files:

  • crates/notedeck/src/persist/settings_handler.rs
  • crates/notedeck/src/app.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to Cargo.toml : Do not vendor code; in Cargo.toml replace the existing URL with the fork that includes the new code. If vendoring is absolutely necessary, present the case why no other options are feasible

Applied to files:

  • vendor/ewebsock/Cargo.toml
  • vendor/ewebsock/.cargo-ok
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `AppContext` to access mutable handles to services (database, caches, relay pool, account state, localization, settings, wallet) rather than accessing global state

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/app.rs : Implement the `App` trait with `update(&mut self, &mut AppContext, &mut egui::Ui) -> AppResponse` to drive egui rendering and signal high-level actions

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Embrace egui idioms in UI code: builder chains, closures returning `Response`, `ui.vertical`/`horizontal` for layout

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Never use Mutexes in UI paths; the render loop must never block. Use `Rc<RefCell<>>` for single-threaded interior mutability, `Arc<tokio::sync::RwLock<>>` for cross-thread sharing, and `poll_promise::Promise` for async results

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `tracing` macros for structured logging and `profiling` scopes where hot paths exist

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : For animations (GIFs, video), track `repaint_at` timestamps and only request repaints when necessary; avoid spinning every frame

Applied to files:

  • crates/notedeck_chrome/src/chrome.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Wrap user-facing strings with `tr!` or `tr_plural!` macros for localization and run `python3 scripts/export_source_strings.py` after changing strings

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
🧬 Code graph analysis (11)
crates/enostr/src/relay/mod.rs (2)
crates/enostr/src/relay/pool.rs (3)
  • new (114-121)
  • new (143-150)
  • url (53-58)
vendor/ewebsock/src/lib.rs (1)
  • connect_with_wakeup (242-250)
vendor/ewebsock/src/web.rs (3)
vendor/ewebsock/src/native_tungstenite.rs (6)
  • drop (31-33)
  • send (40-44)
  • close (49-54)
  • forget (57-60)
  • ws_receive_impl (63-76)
  • ws_connect_impl (148-167)
vendor/ewebsock/src/native_tungstenite_tokio.rs (6)
  • drop (14-16)
  • send (23-27)
  • close (32-37)
  • forget (40-43)
  • ws_receive_impl (155-157)
  • ws_connect_impl (129-135)
vendor/ewebsock/src/lib.rs (1)
  • new (91-93)
vendor/ewebsock/src/native_tungstenite_tokio.rs (3)
vendor/ewebsock/src/tungstenite_common.rs (1)
  • into_requester (20-32)
vendor/ewebsock/src/native_tungstenite.rs (4)
  • send (40-44)
  • close (49-54)
  • forget (57-60)
  • ws_connect_impl (148-167)
vendor/ewebsock/src/lib.rs (1)
  • connect (222-226)
crates/notedeck_chrome/src/chrome.rs (1)
crates/notedeck/src/tor.rs (2)
  • status (89-91)
  • status (287-289)
crates/notedeck/src/app.rs (1)
crates/notedeck/src/tor.rs (2)
  • is_supported (85-87)
  • is_supported (283-285)
crates/notedeck/src/tor.rs (2)
crates/notedeck/src/storage/file_storage.rs (1)
  • path (42-44)
crates/notedeck/src/app.rs (1)
  • new (188-339)
vendor/ewebsock/src/tungstenite_common.rs (1)
vendor/ewebsock/src/lib.rs (1)
  • default (165-176)
crates/notedeck_columns/src/ui/settings.rs (2)
crates/notedeck/src/tor.rs (2)
  • is_supported (85-87)
  • is_supported (283-285)
crates/notedeck/src/ui.rs (1)
  • richtext_small (5-10)
vendor/ewebsock/src/lib.rs (3)
vendor/ewebsock/src/native_tungstenite.rs (2)
  • ws_connect_impl (148-167)
  • ws_receive_impl (63-76)
vendor/ewebsock/src/native_tungstenite_tokio.rs (2)
  • ws_connect_impl (129-135)
  • ws_receive_impl (155-157)
vendor/ewebsock/src/web.rs (2)
  • ws_connect_impl (70-191)
  • ws_receive_impl (65-67)
vendor/ewebsock/src/native_tungstenite.rs (1)
vendor/ewebsock/src/tungstenite_common.rs (1)
  • into_requester (20-32)
crates/notedeck_columns/src/nav.rs (1)
crates/notedeck/src/tor.rs (2)
  • is_supported (85-87)
  • is_supported (283-285)
🪛 LanguageTool
vendor/ewebsock/README.md

[style] ~3-~3: Using many exclamation marks might seem excessive (in this case: 5 exclamation marks for a text that’s 775 characters long)
Context: ...](https://github.com/rerun-io/ewebsock) Latest version Documentation unsafe forbidden Build Status ![MIT](https://img.shields.io/badge/licen...

(EN_EXCESSIVE_EXCLAMATION)

🔇 Additional comments (40)
.cargo/config.toml (1)

4-6: Verify: [env] section applies globally, not just to aarch64-linux-gnu.

The [env] section is a top-level Cargo configuration that sets these environment variables for all targets, not only for aarch64-unknown-linux-gnu. Cargo does not support target-scoped [env] sections.

If global SQLite bundling is intentional (the PR mentions Windows builds also need this), consider adding a clarifying comment or moving this section away from the target-specific block to avoid confusion:

 [target.aarch64-unknown-linux-gnu]
 linker = "aarch64-linux-gnu-gcc"
 
+# Bundle SQLite for all targets to ensure consistent builds
 [env]
 LIBSQLITE3_SYS_BUNDLING = "1"
 LIBSQLITE3_SYS_STATIC = "1"

If bundling was intended only for specific cross-compilation scenarios, you'll need a different approach (e.g., conditional logic in a build.rs or CI-level environment variables).

crates/notedeck/src/context.rs (2)

32-32: LGTM! TorManager correctly integrated into AppContext.

The addition follows the established pattern of exposing services as mutable references through AppContext, which aligns with the coding guidelines for service access.

Consider adding a brief doc comment for the tor field to maintain consistency with documentation practices.


1-5: Import addition is appropriate.

TorManager is correctly imported alongside other service types, maintaining the existing import organization.

vendor/ewebsock/README.md (1)

1-42: Vendored upstream documentation.

This README is part of the vendored ewebsock crate. The static analysis hint about exclamation marks is a false positive—these are standard markdown badge URLs.

The vendoring concern has been raised on the .cargo-ok file.

vendor/ewebsock/Cargo.toml (1)

264-264: Good: unsafe_code = "deny" enforced.

This aligns with the crate's "unsafe forbidden" policy and provides strong safety guarantees.

vendor/ewebsock/.cargo_vcs_info.json (1)

1-6: Good: VCS info preserved for traceability.

The SHA reference (636d3912e1d2e7389f343774a1ad8f8ec77df085) enables tracking the exact upstream commit this vendor snapshot was taken from, which is essential for future updates or security audits.

Cargo.toml (1)

93-97: LGTM on Arti dependencies with pinned revision.

Using a specific git revision for the Arti crates ensures reproducible builds. Since Arti is still evolving, pinning to a known-working commit (as mentioned in PR comments, this resolves the derive-deftly conflict) is appropriate.

Consider documenting in a code comment or README why this specific revision was chosen (e.g., "pinned to fix derive-deftly 1.5 compatibility") to help future maintainers understand if/when to update.

crates/notedeck/src/lib.rs (2)

39-39: LGTM on module organization.

The tor module is correctly placed in its own module tree, following the codebase convention. The alphabetical ordering is maintained.


92-92: Public re-exports look appropriate.

Re-exporting TorManager and TorStatus at the crate root follows the established pattern for other public types. Based on learnings, ensure the tor module itself has adequate docstring coverage for the exported types.

crates/notedeck_chrome/src/chrome.rs (6)

68-73: LGTM on Tor color palette constants.

The color constants follow a clear naming convention and appear to match the Tor brand palette. Using module-level constants is appropriate for these static values.


270-272: LGTM on Tor status retrieval placement.

The status check and indicator display are correctly placed after app action processing, ensuring the indicator reflects the current state. The status() call appears to be a non-blocking read.


891-901: LGTM on indicator styling types.

The TorIndicatorStyle struct cleanly encapsulates all visual properties, and using an enum for OverlayStyle allows future extensibility (e.g., adding pulsing animation for Starting state).


903-916: LGTM on color blending implementation.

The linear interpolation with proper clamping is correct. Using from_rgba_unmultiplied is appropriate since the input colors are in unmultiplied format.


991-992: Hiding the indicator for Unsupported is reasonable.

Returning None for TorStatus::Unsupported hides the badge entirely on platforms where Tor isn't available (Android, wasm). Per PR comments, consider providing guidance in Settings UI for alternatives (e.g., Orbot on Android) rather than silently hiding the feature.


995-1050: LGTM on onion icon painting.

The procedural drawing creates a recognizable onion shape with proper layering. The geometry calculations using radius-based offsets ensure the icon scales correctly with the allocated rect.

crates/notedeck_columns/src/nav.rs (3)

41-44: LGTM on Tor import addition.

The TorManager import from the notedeck crate is appropriate for integrating Tor status and support checks into the navigation and settings flow.


530-537: LGTM on settings action processing.

The additional parameters (ctx.tor and ui.ctx()) passed to process_settings_action correctly propagate Tor manager context, enabling the settings handler to manage Tor state. This follows the coding guideline of using AppContext to access services.


646-655: LGTM on SettingsView Tor integration.

Passing ctx.tor.status() and TorManager::is_supported() to SettingsView enables proper conditional UI rendering based on platform support and current Tor state.

crates/notedeck/src/persist/settings_handler.rs (1)

197-207: LGTM on Tor setting accessors.

The set_use_tor and use_tor methods follow the established pattern of other settings accessors. The setter persists via try_save_settings() which uses TimedSerializer::try_save to avoid blocking the frame, consistent with coding guidelines.

vendor/ewebsock/src/tungstenite_common.rs (2)

1-17: LGTM on WebSocketConfig conversion.

The From<Options> implementation correctly maps max_incoming_frame_size to tungstenite's max_frame_size, with proper handling of usize::MAX as unbounded (None). Other options fields are appropriately handled elsewhere in the connection flow.


19-32: LGTM on request builder helper.

The into_requester function cleanly constructs a ClientRequestBuilder with headers and subprotocols from options. The builder pattern with chained with_header and with_sub_protocol calls is idiomatic.

crates/enostr/src/relay/mod.rs (3)

151-157: LGTM on Options field addition.

Adding the options field to Relay enables dynamic transport configuration, which is essential for the Tor routing feature.


183-200: LGTM on Relay::new signature extension.

The extended signature properly accepts and stores Options, cloning for the connection call since connect_with_wakeup takes ownership. This design allows the relay to reconnect with the same options later.


218-225: LGTM on connect using stored options.

Using self.options.clone() ensures reconnections use the current configuration, enabling transport changes to take effect on reconnect.

crates/enostr/src/relay/pool.rs (4)

10-18: LGTM on imports.

The conditional imports for Transport and Arc are correctly gated for platforms that support Tor (non-wasm32, non-android).


128-133: LGTM on ws_options field.

The ws_options field centralizes WebSocket configuration, enabling consistent options across all relays in the pool.


322-326: LGTM on options propagation.

New relays are created with the pool's current ws_options, ensuring they inherit the configured transport (direct or SOCKS proxy).


350-366: Partial relay reconnection failures are silently logged.

When configure_transport is called (e.g., enabling Tor), relays that fail to reconnect are logged but the method continues. This is resilient but could leave users unaware that some relays are still using direct connections.

Per PR reviewer comments, consider whether partial failures should be surfaced to the UI or whether the status should indicate "some relays failed to switch transport."

The current behavior is acceptable for an initial implementation, but verify this aligns with the desired UX for transport switching. Consider whether configure_transport should return a result indicating partial success/failure.

vendor/ewebsock/src/native_tungstenite_tokio.rs (1)

39-43: LGTM!

The forget implementation correctly prevents the connection from being closed when the sender is dropped, matching the pattern in native_tungstenite.rs.

vendor/ewebsock/src/web.rs (2)

91-148: LGTM!

The callback handling follows standard WASM patterns with proper use of .forget() to keep closures alive. The blob-to-ArrayBuffer conversion via FileReader is a reasonable approach for handling binary data.


193-199: LGTM!

The close_socket helper properly handles errors and provides appropriate logging.

crates/notedeck/src/tor.rs (1)

110-138: LGTM!

The poll() implementation correctly uses try_recv() for non-blocking status updates, which is appropriate for the UI render loop. Status transitions are handled properly.

crates/notedeck_columns/src/ui/settings.rs (2)

499-563: LGTM!

The network_section implementation follows egui idioms with proper layout, uses tr! macros for all user-facing strings, and cleanly handles the unsupported case with an early return.


136-153: LGTM!

The SettingsView constructor properly accepts and stores the new Tor-related fields.

crates/notedeck/src/app.rs (5)

133-136: LGTM!

Polling Tor status every frame with tor.poll() is appropriate since it uses non-blocking try_recv(). The transport update is correctly cfg-gated for supported platforms.


218-226: LGTM!

Tor initialization properly checks is_supported() and the user setting before enabling, with appropriate error handling and setting revert on failure.


384-405: LGTM!

The app_context() method correctly exposes TorManager via AppContext, following the established pattern for service access.


86-88: LGTM!

The new tor and ws_transport fields are correctly added with appropriate cfg-gating for platform support.


431-449: This review comment is incorrect. The configure_transport implementation at crates/enostr/src/relay/pool.rs:351-366 explicitly reconnects all existing relay connections when the transport is updated. It iterates through all relays, calls update_options() with the new transport settings, and then calls connect() to reconnect each relay with the new configuration. The log message "failed to reconnect relay" confirms reconnection is intentional.

Likely an incorrect or invalid review comment.

vendor/ewebsock/src/lib.rs (1)

1-281: Well-structured cross-platform WebSocket API.

The vendored library provides a clean abstraction over platform-specific WebSocket implementations with proper conditional compilation for native vs. WASM targets. The API surface is well-documented, and the SOCKS proxy support through the Transport enum enables the Tor routing feature cleanly.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +80 to +83
let control = on_event(WsEvent::Opened);
if control.is_break() {
log::warn!("ControlFlow::Break not implemented for the tungstenite tokio backend");
}
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

ControlFlow::Break is ignored; consider implementing or documenting the limitation.

The on_event callback can return ControlFlow::Break but the code only logs a warning and continues. The reader's async {} return (line 121) means it never actually breaks out of the loop. If this is intentional, consider returning Result<(), ()> from the callback instead to avoid API confusion.

Also applies to: 118-121

🤖 Prompt for AI Agents
In `@vendor/ewebsock/src/native_tungstenite_tokio.rs` around lines 80 - 83, The
on_event callback return value (used at the WsEvent::Opened call and the reader
closure) currently ignores ControlFlow::Break and merely logs a warning; update
the logic so ControlFlow::Break is honored: have the on_event(...) result be
checked and, if it indicates Break, break out of the reader loop / return from
the async reader task immediately instead of continuing, or change the callback
signature to return Result<(), E> (or bool) and propagate that result so callers
can stop processing; adjust handling around the on_event(WsEvent::Opened) call
and the reader async closure so the Break/Err path causes an immediate exit
rather than continuing.

Comment on lines +32 to +47
pub fn send(&mut self, msg: WsMessage) {
if let Some(socket) = &mut self.socket {
let result = match msg {
WsMessage::Binary(data) => {
socket.set_binary_type(web_sys::BinaryType::Blob);
socket.send_with_u8_array(&data)
}
WsMessage::Text(text) => socket.send_with_str(&text),
unknown => {
panic!("Don't know how to send message: {unknown:?}");
}
};
if let Err(err) = result.map_err(string_from_js_value) {
log::error!("Failed to send: {err:?}");
}
}
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

Ping and Pong messages are not handled and will panic.

The send() method only handles Binary and Text, but panics on Ping, Pong, and Unknown. The native implementations support Ping/Pong. Consider handling these or documenting the limitation:

♻️ Suggested handling
                 WsMessage::Binary(data) => {
                     socket.set_binary_type(web_sys::BinaryType::Blob);
                     socket.send_with_u8_array(&data)
                 }
                 WsMessage::Text(text) => socket.send_with_str(&text),
+                WsMessage::Ping(_) | WsMessage::Pong(_) => {
+                    log::debug!("Ping/Pong not supported in WASM WebSocket");
+                    return;
+                }
                 unknown => {
                     panic!("Don't know how to send message: {unknown:?}");
                 }
🤖 Prompt for AI Agents
In `@vendor/ewebsock/src/web.rs` around lines 32 - 47, The send() method in web.rs
currently panics for WsMessage::Ping, WsMessage::Pong and other unknown
variants; update the match in send() (inside the send method) to handle
WsMessage::Ping and WsMessage::Pong instead of falling through to the panic: add
explicit arms for WsMessage::Ping and WsMessage::Pong that either translate them
to an appropriate send call on socket (e.g., send a small binary/frame or text
payload if your websocket backend requires) or safely no-op/log the event with
log::debug!/log::warn! so they don't panic, and add a final arm for
WsMessage::Unknown that logs and returns an error instead of panicking;
reference the send function and the WsMessage enum variants when making the
change.

@alltheseas
Copy link
Copy Markdown
Contributor Author

MacOS Screenshot 2026-01-14 at 10 15 12 PM

@alltheseas
Copy link
Copy Markdown
Contributor Author

image android

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

🤖 Fix all issues with AI agents
In
`@crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java`:
- Line 14: DEFAULT_SOCKS_PORT is hardcoded to 9150 which may collide with Tor
Browser; modify TorManager to detect and handle port conflicts in
startSocksProxy: instead of always using DEFAULT_SOCKS_PORT, probe for an
available port (e.g., try binding a ServerSocket to ports starting at 9150 and
incrementing) or accept a configurable port parameter, and if the chosen port is
already in use throw/log a clear, non-silent error that includes the conflicting
port and suggestion to pick another port; update references to
DEFAULT_SOCKS_PORT and the startSocksProxy method to use the resolved available
port or the provided parameter.

In `@crates/notedeck_columns/src/ui/settings.rs`:
- Around line 101-117: The UI mutates Settings.use_tor immediately via the
toggle_value widget causing a desync if tor.set_enabled(...) (handled by the
ToggleTor action) fails; change the UI to not directly mutate the Settings
struct on click but instead: keep the toggle's displayed state bound to
settings.use_tor, emit the ToggleTor action when clicked (do not call
settings.set_use_tor() from the widget), and in the ToggleTor handler (where
tor.set_enabled(enabled) is called) update settings.set_use_tor(enabled) only on
Ok, and on Err explicitly revert any UI-visible state if needed (e.g., re-emit a
state update or call settings.set_use_tor(previous_value>) so the widget
reflects the real state; refer to the ToggleTor enum variant, tor.set_enabled,
settings.set_use_tor, the toggle_value widget, the Settings struct and the
SettingsHandler when applying the change.

In `@crates/notedeck/Cargo.toml`:
- Around line 57-58: The keyring dependency causes WASM/Android build failures;
move the keyring = { workspace = true } entry out of the main [dependencies] and
into a platform-gated section such as [target.'cfg(all(not(target_arch =
"wasm32"), not(target_os = "android")))'.dependencies] so it is only compiled
for non-wasm/non-android targets, or replace it with a WASM-compatible
implementation and conditionally #[cfg(...)] use that in code paths like app.rs,
media/network.rs, and tor.rs.

In `@crates/notedeck/src/media/network.rs`:
- Around line 297-301: The status code parsing silently falls back to 0 on
malformed status lines (status_line -> status_code) which bypasses the redirect
check and can hide errors; update the parser in network.rs to treat parse
failures as an error instead of using unwrap_or(0): attempt to extract and parse
the status token from status_line, and if missing or parse fails
return/propagate an appropriate Err (or Result::Err) from the surrounding
function (rather than assigning 0), so callers can handle malformed responses
and the redirect check ((300..400).contains(&status_code)) won't operate on an
invalid value.
- Around line 39-46: The comment on http_fetch is misleading: http_req actually
returns Unsupported on wasm32/android so HTTP is entirely unsupported there (not
just SOCKS proxy). Update the comment above pub async fn http_fetch(...) to
state that HTTP requests are not supported on wasm32/android (e.g., "HTTP
requests are not supported on these platforms; http_req returns Unsupported") or
else implement a platform-specific alternative using native APIs; reference the
http_fetch wrapper and the http_req implementation to ensure the comment or
alternative accurately reflects that full HTTP is unsupported on these targets.

In `@tools/arti-build/build-arti.sh`:
- Around line 124-127: The STRIP path logic in build-arti.sh only checks
darwin-x86_64 and linux-x86_64 and misses Apple Silicon; update the STRIP
resolution (the STRIP variable assignment and its subsequent if fallback) to
also test the darwin-aarch64 (or darwin-arm64 if your NDK uses that naming)
prebuilt path under $ANDROID_NDK_HOME/toolchains/llvm/prebuilt before falling
back to the linux path so llvm-strip is found on M1/M2 Macs.
🧹 Nitpick comments (18)
crates/enostr/src/relay/pool.rs (1)

350-366: Consider returning reconnection results for better error visibility.

The current implementation logs reconnection failures but doesn't inform the caller which relays failed. This aligns with the PR comment concern about "transport switch leaves existing relay sockets direct"—if connect() fails, that relay remains on the old transport.

Consider returning a summary of failures or at least a boolean indicating partial success:

♻️ Suggested enhancement
 #[cfg(not(target_arch = "wasm32"))]
 pub fn configure_transport(
     &mut self,
     transport: Transport,
     wakeup: Arc<dyn Fn() + Send + Sync>,
-) {
+) -> Vec<String> {
+    let mut failed_relays = Vec::new();
     self.ws_options.transport = transport;
     for relay in &mut self.relays {
         if let PoolRelay::Websocket(wsr) = relay {
             wsr.relay.update_options(self.ws_options.clone());
             let wake = wakeup.clone();
             if let Err(err) = wsr.relay.connect(move || (*wake)()) {
                 error!("failed to reconnect relay {}: {err}", wsr.relay.url);
+                failed_relays.push(wsr.relay.url.to_string());
             }
         }
     }
+    failed_relays
 }

This would allow the UI layer to show which relays couldn't switch to Tor, addressing the reviewer's concern about toggle desync.

crates/notedeck_chrome/android/app/build.gradle (1)

23-25: Consider enabling minification for release builds.

minifyEnabled false in release builds increases APK size and exposes more code to reverse engineering. Unless there's a specific reason (e.g., debugging production issues), consider enabling it:

 release {
-    minifyEnabled false
+    minifyEnabled true
+    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
 }
crates/notedeck/src/jni_cache.rs (1)

18-33: Global state used for JNI cache - document the exception.

The coding guidelines state "Global variables are not allowed in this codebase." While this static JNI_CACHE is necessary for JNI interop (Java needs a stable entry point to call into Rust), consider adding a module-level doc comment explicitly documenting why this is an exception to the rule.

The manual Send + Sync implementations appear correct based on the safety justification, but this is a sensitive pattern.

Based on learnings, global variables are prohibited. Please confirm this JNI use case is an acceptable exception, or consider whether the cache could be managed differently (e.g., passed through JNI calls).

tools/arti-build/src/lib.rs (2)

497-500: Consider explicit shutdown for cleaner connection termination.

When one direction of the bidirectional copy completes, the other direction is abruptly dropped. For cleaner TCP termination, consider explicitly shutting down the write half of the remaining connection.

Optional improvement for graceful shutdown
     tokio::select! {
-        _ = client_to_tor => {}
-        _ = tor_to_client => {}
+        result = client_to_tor => {
+            if result.is_ok() {
+                let _ = tor_write.shutdown().await;
+            }
+        }
+        result = tor_to_client => {
+            if result.is_ok() {
+                let _ = client_write.shutdown().await;
+            }
+        }
     }

58-78: Nested lock acquisition in log_to_java could deadlock.

The log_to_java function acquires LOG_CALLBACK lock while it could potentially be called from code that already holds the STATE lock (e.g., during error handling in initialize). While the current call sites appear safe, this pattern is fragile.

Consider using try_lock() instead of lock() for the log callback to avoid potential deadlock:

 fn log_to_java(message: &str) {
     if let Some(callback_lock) = LOG_CALLBACK.get() {
-        if let Ok(callback_guard) = callback_lock.lock() {
+        if let Ok(callback_guard) = callback_lock.try_lock() {
crates/notedeck/src/media/network.rs (2)

377-388: Silent failure on malformed chunk size.

If the chunk size line is malformed (not valid hex), unwrap_or(0) treats it as end-of-body. This could silently return an incomplete response instead of erroring.

Consider returning an error on parse failure
-        let chunk_size = usize::from_str_radix(size_line.trim(), 16).unwrap_or(0);
-
-        if chunk_size == 0 {
-            break;
-        }
+        let size_str = size_line.trim();
+        // Empty line or "0" indicates end of chunks
+        if size_str.is_empty() || size_str == "0" {
+            break;
+        }
+        let chunk_size = usize::from_str_radix(size_str, 16)
+            .map_err(|_| HyperHttpError::InvalidRedirectLocation)?; // Consider adding a dedicated error variant

198-267: Duplicate redirect handling logic in HTTPS/HTTP branches.

The redirect handling code (lines 219-235 and 245-261) is nearly identical between the HTTPS and HTTP branches. Consider extracting the redirect handling into a helper or restructuring to reduce duplication.

This is a maintainability nit - the current code is correct and functional.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorProvider.java (1)

1-57: Clean interface design for Tor provider abstraction.

The interface is well-structured with clear Javadoc documentation. Consider adding Android's @Nullable annotation to getSocksProxy() return type and setLogCallback() parameter to make the nullability contract explicit for consumers.

import androidx.annotation.Nullable;
// ...
`@Nullable`
String getSocksProxy();
// ...
void setLogCallback(`@Nullable` TorLogCallback callback);
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/NativeTorProvider.java (1)

60-75: Minor inconsistency: getSocksProxy() queries native instead of using cached socksPort.

startSocksProxy() stores the port in socksPort field (line 37), but getSocksProxy() queries ArtiNative.getSocksPort() instead of using the cached value. This is actually safer since it reflects the true native state, but the local socksPort field becomes partially redundant. Consider either:

  1. Removing the socksPort field entirely and always querying native, or
  2. Using socksPort here for consistency (only if you trust the local state)

The current approach (querying native) is the safer choice for correctness.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (2)

70-86: Directory creation failures are silently ignored.

mkdirs() can fail (permissions, full disk), but the return value is discarded. This may cause a confusing failure in provider.initialize() downstream.

♻️ Suggested improvement
             // Create directories
-            cacheDir.mkdirs();
-            stateDir.mkdirs();
+            if (!cacheDir.mkdirs() && !cacheDir.exists()) {
+                Log.e(TAG, "Failed to create cache directory: " + cacheDir);
+                return false;
+            }
+            if (!stateDir.mkdirs() && !stateDir.exists()) {
+                Log.e(TAG, "Failed to create state directory: " + stateDir);
+                return false;
+            }

64-90: start() is not thread-safe.

While getInstance() is synchronized, start() is not. Concurrent calls could race on the initialized flag, potentially calling provider.initialize() twice.

♻️ Consider synchronizing start/stop
-    public boolean start(int socksPort) {
+    public synchronized boolean start(int socksPort) {

Or use a more granular lock if synchronizing the entire method is too coarse.

crates/notedeck_chrome/src/chrome.rs (4)

920-930: Missing docstrings on new types.

Per coding guidelines, new code should have docstring coverage. Consider adding brief documentation for TorIndicatorStyle and OverlayStyle.

📝 Suggested documentation
+/// Style configuration for the Tor status indicator badge.
 struct TorIndicatorStyle {
+    /// Primary badge color
     base: Color32,
+    /// Secondary/highlight color
     accent: Color32,
+    /// Color for the onion stem/sprout
     sprout: Color32,
+    /// Optional overlay (e.g., slash for disabled state)
     overlay: Option<OverlayStyle>,
+    /// Localized tooltip text
     tooltip: String,
 }

+/// Visual overlay styles for the Tor indicator.
 enum OverlayStyle {
+    /// Diagonal slash overlay with specified color
     Slash(Color32),
 }

932-945: Consider using Color32::lerp or add docstring.

egui::Color32 has a lerp method that does the same thing. If you prefer the custom implementation for clarity, please add a docstring explaining the function.


947-971: Indicator is hover-only; no click affordance.

The badge shows a tooltip on hover but has no click handler for retry or detailed info. This was noted in PR comments as a potential UX gap. Consider whether a click action (e.g., navigate to settings, retry connection) would improve usability.


997-1008: Error tooltip exposes raw error string.

The TorStatus::Failed(err) branch interpolates err.as_str() directly into the tooltip. While less visible than main UI text, consider whether internal Arti error strings are appropriate for end users.

♻️ Alternative: generic error with logged details
         TorStatus::Failed(err) => Some(TorIndicatorStyle {
             base: TOR_ALERT,
             accent: TOR_AMBER,
             sprout: TOR_AMBER,
             overlay: Some(OverlayStyle::Slash(Color32::from_rgb(250, 226, 226))),
-            tooltip: tr!(
-                i18n,
-                "Tor routing failed: {error}",
-                "Tooltip when Tor routing has failed",
-                error = err.as_str()
-            ),
+            tooltip: {
+                tracing::debug!("Tor failure details: {}", err.as_str());
+                tr!(
+                    i18n,
+                    "Tor routing failed. Check settings to retry.",
+                    "Tooltip when Tor routing has failed"
+                )
+            },
         }),
crates/notedeck_columns/src/ui/settings.rs (1)

574-640: LGTM with minor suggestion: Network section implementation.

Good use of settings_group, proper localization with tr!, and clean status mapping. The TorStatus::Failed handling correctly logs details while showing a user-friendly message.

One minor enhancement per PR comments: the Unsupported message could suggest alternatives like Orbot on Android.

💡 Optional: Suggest alternatives for unsupported platforms
                     TorStatus::Unsupported => tr!(
                         self.note_context.i18n,
-                        "Tor is not available on this platform.",
+                        "Tor is not available on this platform. Consider using Orbot or a system-wide VPN.",
                         "Status label when Tor is unsupported"
                     ),
crates/notedeck/src/tor.rs (2)

513-515: Consider logging directory creation failures.

Silently ignoring create_dir_all errors could mask permission issues or disk space problems. While the Java side also creates these directories (per the TorManager.java snippet), consider at least logging warnings:

♻️ Suggested improvement
             // Create directories
-            let _ = std::fs::create_dir_all(&self.cache_dir);
-            let _ = std::fs::create_dir_all(&self.state_dir);
+            if let Err(e) = std::fs::create_dir_all(&self.cache_dir) {
+                tracing::warn!("Failed to create tor cache dir: {e}");
+            }
+            if let Err(e) = std::fs::create_dir_all(&self.state_dir) {
+                tracing::warn!("Failed to create tor state dir: {e}");
+            }

478-494: Potential status desync if Java-side Tor crashes after startup.

The poll() method only monitors state during Starting. Once Running is reached, there's no health check to detect if the Java Tor runtime crashes or becomes unresponsive. The PR objectives highlight this "toggle desync" concern.

Consider periodically verifying the SOCKS port remains accessible, or having the Java side expose a health/status method that can be polled:

♻️ Possible approach
pub fn poll(&mut self) {
    if self.jni_failed {
        return;
    }

    match &self.status {
        TorStatus::Starting => {
            if let Some(port) = self.get_socks_port() {
                if port > 0 {
                    self.status = TorStatus::Running {
                        socks_port: port as u16,
                    };
                }
            }
        }
        TorStatus::Running { .. } => {
            // Periodically verify Tor is still running
            if let Some(port) = self.get_socks_port() {
                if port == 0 {
                    tracing::warn!("Tor appears to have stopped");
                    self.status = TorStatus::Failed("Tor stopped unexpectedly".to_string());
                }
            }
        }
        _ => {}
    }
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f00bea3 and e048ca6.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tools/arti-build/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • .beads/issues.jsonl
  • .cargo/config.toml
  • .gitignore
  • Cargo.toml
  • Makefile
  • crates/enostr/src/relay/mod.rs
  • crates/enostr/src/relay/pool.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/app.rs
  • crates/notedeck/src/context.rs
  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/jni_cache.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/media/gif.rs
  • crates/notedeck/src/media/network.rs
  • crates/notedeck/src/media/static_imgs.rs
  • crates/notedeck/src/persist/settings_handler.rs
  • crates/notedeck/src/tor.rs
  • crates/notedeck_chrome/android/app/build.gradle
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/ArtiNative.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/NativeTorProvider.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/StubTorProvider.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorLogCallback.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorProvider.java
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • tools/arti-build/ARTI_VERSION
  • tools/arti-build/Cargo.toml
  • tools/arti-build/build-arti.sh
  • tools/arti-build/src/lib.rs
✅ Files skipped from review due to trivial changes (3)
  • tools/arti-build/ARTI_VERSION
  • .beads/issues.jsonl
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/notedeck/src/persist/settings_handler.rs
  • .cargo/config.toml
🧰 Additional context used
📓 Path-based instructions (4)
crates/notedeck*/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

crates/notedeck*/src/**/*.rs: Use AppContext to access mutable handles to services (database, caches, relay pool, account state, localization, settings, wallet) rather than accessing global state
Never use Mutexes in UI paths; the render loop must never block. Use Rc<RefCell<>> for single-threaded interior mutability, Arc<tokio::sync::RwLock<>> for cross-thread sharing, and poll_promise::Promise for async results
Wrap async work in poll_promise::Promise, check with promise.ready() or promise.ready_mut() each frame—never block the render loop
Use tokio::sync::RwLock instead of Arc<Mutex<>> for cross-thread sharing in Notedeck
Persist state via TimedSerializer::try_save to avoid blocking the frame; batch mutations with SettingsHandler::update_batch
Wrap user-facing strings with tr! or tr_plural! macros for localization and run python3 scripts/export_source_strings.py after changing strings
Use JobPool for CPU-bound work and return results via tokio::sync::oneshot wrapped in Promises; use tokio::spawn() for network I/O and relay sync
Mark performance-critical functions with #[profiling::function] for visibility in the puffin profiler
Use tracing macros for structured logging and profiling scopes where hot paths exist
Prefer early returns and guard clauses over deeply nested conditionals; simplify control flow by exiting early instead of wrapping logic in multiple layers of if statements (Nevernesting principle)
Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference
Ensure docstring coverage for any code added or modified
Avoid Mutexes in Notedeck code; prefer poll_promise::Promise for async results, Rc<RefCell<>> for single-threaded interior mutability, or tokio::sync::RwLock when cross-thread sharing is truly necessary
Never block the render loop; use Promise::ready() for non-blocking result checks. Offload CPU-heavy work to...

Files:

  • crates/notedeck/src/context.rs
  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/media/gif.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/tor.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/media/static_imgs.rs
  • crates/notedeck/src/jni_cache.rs
  • crates/notedeck/src/media/network.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
crates/notedeck*/src/lib.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use strict Rust 2021 edition; ensure edition-lints are strict and clippy disallowed_methods is denied at crate root to enforce API hygiene

Files:

  • crates/notedeck/src/lib.rs
Cargo.toml

📄 CodeRabbit inference engine (AGENTS.md)

Do not vendor code; in Cargo.toml replace the existing URL with the fork that includes the new code. If vendoring is absolutely necessary, present the case why no other options are feasible

Files:

  • Cargo.toml
crates/notedeck*/src/app.rs

📄 CodeRabbit inference engine (AGENTS.md)

Implement the App trait with update(&mut self, &mut AppContext, &mut egui::Ui) -> AppResponse to drive egui rendering and signal high-level actions

Files:

  • crates/notedeck/src/app.rs
🧠 Learnings (18)
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `AppContext` to access mutable handles to services (database, caches, relay pool, account state, localization, settings, wallet) rather than accessing global state

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/app.rs : Implement the `App` trait with `update(&mut self, &mut AppContext, &mut egui::Ui) -> AppResponse` to drive egui rendering and signal high-level actions

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `tokio::sync::RwLock` instead of `Arc<Mutex<>>` for cross-thread sharing in Notedeck

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Embrace egui idioms in UI code: builder chains, closures returning `Response`, `ui.vertical`/`horizontal` for layout

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/lib.rs : Use strict Rust 2021 edition; ensure edition-lints are strict and clippy `disallowed_methods` is denied at crate root to enforce API hygiene

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Never use Mutexes in UI paths; the render loop must never block. Use `Rc<RefCell<>>` for single-threaded interior mutability, `Arc<tokio::sync::RwLock<>>` for cross-thread sharing, and `poll_promise::Promise` for async results

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `tracing` macros for structured logging and `profiling` scopes where hot paths exist

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Persist state via `TimedSerializer::try_save` to avoid blocking the frame; batch mutations with `SettingsHandler::update_batch`

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Prefer module-level organization over monolithic files; each feature (accounts, decks, timelines, media) should live in its own module tree

Applied to files:

  • crates/notedeck/src/context.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : For animations (GIFs, video), track `repaint_at` timestamps and only request repaints when necessary; avoid spinning every frame

Applied to files:

  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/media/gif.rs
  • crates/notedeck_chrome/src/chrome.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Ensure docstring coverage for any code added or modified

Applied to files:

  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/tor.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Avoid Mutexes in Notedeck code; prefer `poll_promise::Promise` for async results, `Rc<RefCell<>>` for single-threaded interior mutability, or `tokio::sync::RwLock` when cross-thread sharing is truly necessary

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Tests should live alongside modules (e.g., in test submodules), often using `#[tokio::test]` when async behavior is involved

Applied to files:

  • crates/notedeck/src/lib.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Never block the render loop; use `Promise::ready()` for non-blocking result checks. Offload CPU-heavy work to `JobPool` or `tokio::spawn()`, returning results via channels or Promises

Applied to files:

  • crates/notedeck/src/tor.rs
  • crates/notedeck/src/app.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Wrap user-facing strings with `tr!` or `tr_plural!` macros for localization and run `python3 scripts/export_source_strings.py` after changing strings

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to Cargo.toml : Do not vendor code; in Cargo.toml replace the existing URL with the fork that includes the new code. If vendoring is absolutely necessary, present the case why no other options are feasible

Applied to files:

  • Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `JobPool` for CPU-bound work and return results via `tokio::sync::oneshot` wrapped in Promises; use `tokio::spawn()` for network I/O and relay sync

Applied to files:

  • crates/notedeck/src/app.rs
  • crates/enostr/src/relay/pool.rs
🧬 Code graph analysis (10)
crates/notedeck/src/context.rs (1)
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (1)
  • TorManager (12-134)
crates/notedeck/src/imgcache.rs (2)
crates/notedeck/src/media/gif.rs (1)
  • set_http_config (129-131)
crates/notedeck/src/media/static_imgs.rs (1)
  • set_http_config (47-49)
crates/notedeck/src/media/gif.rs (2)
crates/notedeck/src/imgcache.rs (5)
  • new (37-47)
  • new (114-139)
  • new (290-300)
  • set_http_config (50-53)
  • set_http_config (308-310)
crates/notedeck/src/media/static_imgs.rs (2)
  • new (38-44)
  • set_http_config (47-49)
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/NativeTorProvider.java (1)
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/ArtiNative.java (1)
  • ArtiNative (6-83)
crates/notedeck/src/tor.rs (3)
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (1)
  • TorManager (12-134)
crates/notedeck/src/app.rs (1)
  • new (198-360)
crates/notedeck/src/jni_cache.rs (2)
  • with_jni (152-166)
  • find_class (106-148)
crates/notedeck_columns/src/ui/settings.rs (2)
crates/notedeck/src/tor.rs (3)
  • is_supported (128-130)
  • is_supported (380-382)
  • is_supported (452-455)
crates/notedeck/src/ui.rs (1)
  • richtext_small (5-10)
crates/notedeck/src/media/static_imgs.rs (2)
crates/notedeck/src/imgcache.rs (5)
  • new (37-47)
  • new (114-139)
  • new (290-300)
  • set_http_config (50-53)
  • set_http_config (308-310)
crates/notedeck/src/media/gif.rs (2)
  • new (120-126)
  • set_http_config (129-131)
tools/arti-build/src/lib.rs (1)
crates/notedeck/src/jobs/job_pool.rs (1)
  • oneshot (52-52)
crates/notedeck/src/app.rs (1)
crates/notedeck/src/tor.rs (6)
  • is_supported (128-130)
  • is_supported (380-382)
  • is_supported (452-455)
  • socks_proxy (153-159)
  • socks_proxy (392-394)
  • socks_proxy (470-476)
crates/enostr/src/relay/mod.rs (1)
crates/enostr/src/relay/pool.rs (2)
  • new (114-121)
  • new (143-150)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test (Windows) / run
  • GitHub Check: Test (Linux) / run
  • GitHub Check: Test (macOS) / run
  • GitHub Check: Rustfmt + Clippy
  • GitHub Check: Check (android)
🔇 Additional comments (63)
crates/enostr/src/relay/mod.rs (2)

156-156: LGTM! Clean integration of connection options.

The Options field is correctly stored on the Relay struct and propagated through new(). Cloning on connect is appropriate since ewebsock::connect_with_wakeup takes ownership.

Also applies to: 184-199


218-234: LGTM! Proper separation of option update and reconnection.

The connect method correctly uses the stored options, and update_options appropriately only updates the configuration without triggering reconnection—giving callers explicit control over when to reconnect.

crates/enostr/src/relay/pool.rs (3)

10-18: LGTM! Appropriate conditional imports.

The Transport and Arc imports are correctly gated for non-wasm32 targets, aligning with the platform-specific Tor support.


128-133: LGTM! Clean field addition and initialization.

The ws_options field is properly encapsulated as private and initialized with sensible defaults.

Also applies to: 141-150


322-326: LGTM! New relays inherit pool's transport configuration.

Passing self.ws_options.clone() ensures newly added relays use the current transport settings (e.g., SOCKS proxy for Tor).

tools/arti-build/Cargo.toml (1)

1-44: LGTM! Well-structured Android Arti build crate.

The crate is properly configured for building a cdylib targeting Android:

  • Git dependencies use the official Arti repository with a pinned revision matching the workspace
  • Feature selection (rustls over native-tls) avoids OpenSSL cross-compilation complexity
  • Release profile is appropriately optimized for mobile binary size
Cargo.toml (2)

106-110: TLS backend inconsistency between desktop and Android builds.

Line 110 specifies native-tls for tor-rtcompat (desktop), while tools/arti-build/Cargo.toml uses rustls for Android. This is likely intentional (avoiding OpenSSL cross-compilation on Android), but worth documenting to prevent confusion during maintenance.

Additionally, verify that both TLS backends are tested, as they may have different certificate validation behaviors.


140-141: ewebsock fork usage is now properly configured.

The dependency correctly uses a git-based fork reference instead of vendoring, addressing the previous review concern. Ensure this fork is maintained or upstream changes are periodically merged.

crates/notedeck/Cargo.toml (1)

57-73: Target-specific Tor dependencies correctly configured.

The platform gating (not(wasm32), not(android)) properly restricts full Arti runtime to desktop platforms, aligning with the PR's design where Android/WASM use stub modules.

Makefile (2)

46-50: Well-structured Tor-enabled Android build target.

The android-tor target correctly:

  • Chains arti-arm64 and jni dependencies
  • Adds appropriate logcat filters for Tor-related logging (NativeTorProvider, TorManager)
  • Maintains consistency with the non-Tor android target

17-19: The target name arti-arm64 is accurate and not misleading. The --release flag in build-arti.sh explicitly restricts the build to ARM64 only (line 12 and lines 32-35 show RELEASE_TARGETS contains only arm64-v8a:aarch64-linux-android). No changes needed.

Likely an incorrect or invalid review comment.

tools/arti-build/build-arti.sh (1)

1-56: LGTM!

The script structure, argument parsing, and prerequisite checks are well-implemented with proper error handling via set -e and clear usage documentation.

crates/notedeck/src/jni_cache.rs (2)

106-148: LGTM!

The find_class implementation correctly handles:

  • Exception clearing before and after JNI calls
  • Local reference creation for thread safety
  • Proper error propagation with descriptive messages

150-176: LGTM!

The with_jni helper correctly attaches the current thread to the JVM, and the non-Android stubs provide appropriate no-op behavior for cross-platform compilation.

tools/arti-build/src/lib.rs (1)

220-247: Stop function doesn't clear the Tor client.

The stop function clears proxy_task, shutdown_tx, and socks_port, but leaves client intact. This means isInitialized() will still return true after stop, and subsequent startSocksProxy calls will reuse the old client. Verify this is the intended behavior.

If the intent is to allow restart without re-bootstrap, this is fine. If not, consider also clearing the client:

         state.socks_port = None;
+        // Optionally clear client to force re-initialization
+        // state.client = None;
         log_to_java("SOCKS proxy stopped");
crates/notedeck/src/media/network.rs (1)

156-181: LGTM on URL parsing and path handling.

The URL parsing correctly handles query strings and empty paths, defaulting to "/" when needed.

crates/notedeck/src/context.rs (1)

33-33: LGTM!

Adding TorManager to AppContext follows the established pattern for accessing services through the context struct, consistent with the coding guidelines. This ensures Tor state is managed through the proper service architecture rather than global state.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java (2)

34-36: LGTM!

The native method declaration for JNI classloader cache initialization is properly defined as static and private, which is appropriate for internal JNI bridging.


166-175: LGTM!

The JNI classloader initialization is correctly placed after super.onCreate() (which loads the native library) and wrapped with appropriate error handling. The UnsatisfiedLinkError catch ensures the app doesn't crash if the native method isn't available, which aligns with the PR's note that Android uses a stub module.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorLogCallback.java (1)

1-13: LGTM!

Clean, well-documented callback interface following Java conventions. The single-method design is appropriate for log forwarding from the native Tor layer.

crates/notedeck/src/imgcache.rs (3)

4-4: LGTM!

Import correctly added to support HTTP configuration propagation.


48-53: LGTM!

The set_http_config method correctly propagates the HTTP configuration to both static and animated image caches. The clone() on the first call is appropriate since the config needs to be passed to both caches. Good docstring coverage.


307-310: LGTM!

Clean delegation to the underlying TexturesCache, with appropriate documentation. This provides the expected API surface for configuring HTTP behavior (e.g., SOCKS proxy for Tor) on the Images facade.

crates/notedeck/src/lib.rs (4)

16-16: LGTM!

JNI cache module correctly declared as public for cross-crate access from the Android layer.


40-40: LGTM!

Tor module correctly declared as public, enabling Tor integration across the crate boundary.


68-69: LGTM!

HttpConfig is appropriately re-exported alongside other media types, enabling consumers to configure HTTP behavior (e.g., SOCKS proxy for Tor routing) without deep module imports.


94-94: LGTM!

TorManager and TorStatus are correctly re-exported at the crate root, making the Tor integration types accessible to dependent crates (notedeck_chrome, notedeck_columns).

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/StubTorProvider.java (1)

1-48: LGTM!

Clean null object pattern implementation that allows the codebase to gracefully handle missing native Tor support. All methods consistently return appropriate "not available" values (false, null), and the class is well-documented.

Per the PR reviewer comments, consider providing user-facing guidance (e.g., suggesting Orbot as an alternative) when isSupported() returns false on Android. This is a UX improvement that could be addressed in follow-up work rather than blocking this PR.

crates/notedeck/src/media/gif.rs (2)

116-131: LGTM! HTTP config integration follows established patterns.

The http_config field and setter align with the pattern in static_imgs.rs. The implementation correctly propagates HTTP configuration (including SOCKS proxy for Tor) through the async network fetch path without blocking the render loop.


216-231: Async fetch correctly integrates SOCKS proxy configuration.

The http_fetch call properly extracts the SOCKS proxy from config. One minor note: the error message at line 228 may expose internal HTTP error details to users if surfaced in UI. Consider whether this error propagates to user-visible contexts.

crates/notedeck_columns/src/nav.rs (3)

40-43: TorManager correctly imported and accessed via AppContext.

The import and usage pattern follows the coding guidelines by accessing Tor state through AppContext rather than global state.


589-597: Settings action handler correctly wired with Tor context.

The ctx.tor reference is appropriately passed to process_settings_action, enabling Tor toggle functionality in settings.


711-719: SettingsView receives Tor status for UI rendering.

The view construction correctly receives both runtime status (ctx.tor.status()) and platform capability (TorManager::is_supported()), enabling appropriate UI presentation for Tor toggle state.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/ArtiNative.java (2)

6-18: JNI library loading with proper error capture.

The static initializer correctly handles UnsatisfiedLinkError and captures the error message for diagnostic purposes. This pattern allows graceful degradation when the native library isn't available.


56-61: Note: getSocksPort() returns int, converted to String in provider.

The native method returns int (or -1 if not running), while TorProvider.getSocksProxy() returns String. The conversion happens in NativeTorProvider.getSocksProxy() (lines 66-70), which is appropriate separation of concerns.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/NativeTorProvider.java (2)

8-11: Implementation setup looks good.

The TAG constant and socksPort tracking field are appropriately scoped.


12-44: Defensive programming with library guards and exception handling.

All methods properly check ArtiNative.isLibraryLoaded() before invoking native methods, and wrap calls in try-catch blocks. This ensures graceful handling of native library failures.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (1)

92-134: LGTM: Clean delegation pattern.

The remaining methods correctly delegate to the provider. Good documentation coverage.

crates/notedeck_chrome/src/chrome.rs (3)

75-80: LGTM: Tor color palette constants.

The color constants follow the Tor visual identity (purple, lavender, etc.) and provide clear semantic naming for different states.


1024-1079: LGTM: Custom onion icon painting.

The vector drawing logic for the Tor onion badge is well-structured with clear geometric calculations. The overlay slash provides good visual feedback for disabled/error states.


278-280: LGTM: Integration point for Tor indicator.

The indicator is correctly rendered after the app content update, using AppContext to access Tor status as per coding guidelines.

crates/notedeck/src/media/static_imgs.rs (4)

25-29: LGTM: HttpConfig struct for proxy configuration.

Clean, minimal struct for HTTP configuration. Clone and Default derives are appropriate for the use case.


46-49: LGTM: HttpConfig setter with documentation.

The setter follows the same pattern as AnimatedImgTexCache::set_http_config (per gif.rs snippets), maintaining consistency across the codebase.


97-114: LGTM: Config propagation to async fetch.

The http_config is correctly cloned before being moved into the async closure, avoiding borrow checker issues while ensuring the fetch uses the current configuration.


146-161: LGTM: SOCKS proxy integration in network fetch.

The proxy address is correctly passed to http_fetch. Using as_deref() is idiomatic for converting Option<String> to Option<&str>.

crates/notedeck_columns/src/ui/settings.rs (3)

38-38: LGTM: New SettingsAction variant for Tor toggle.

The ToggleTor(bool) variant is appropriately added to handle the network setting change.


123-129: LGTM: SettingsView extended with Tor state.

The new fields cleanly separate the current status (for display) from the support flag (for conditional rendering).


823-828: LGTM: Network section integrated into settings UI.

The section is correctly placed in the UI flow with consistent spacing.

crates/notedeck/src/app.rs (7)

30-33: LGTM: Platform-gated imports for transport types.

Correct use of cfg attributes to import WsTransport and SocksOptions only on non-wasm32 targets.


89-91: LGTM: Tor fields added to Notedeck struct.

The TorManager and WsTransport fields are appropriately added with correct platform gating.


230-241: LGTM: Tor initialization with error recovery.

Good pattern: attempt to enable Tor based on persisted settings, and revert the setting if initialization fails. This prevents startup loops where Tor always fails but settings say it should be enabled.


481-493: LGTM: Platform-specific crypto provider installation.

Using ring on desktop for rustls is standard. The no-op on Android/WASM is acceptable since those platforms have their own crypto implementations or don't need TLS for this use case.


405-427: LGTM: TorManager exposed via AppContext.

Per coding guidelines, services should be accessed through AppContext. Adding tor: &mut self.tor follows this pattern correctly.


453-478: No changes needed. The transport switch properly reconnects existing relay sockets by calling connect() on each relay after updating transport options. New WebSocket connections are established that use the updated transport configuration, replacing the old connections.


143-146: No issue found—tor.poll() is non-blocking.

All platform-specific implementations use only non-blocking operations: the non-wasm32 variant checks a channel with try_recv(), the wasm32 variant is an empty stub, and the Android variant performs a simple JNI state query via getSocksPort(). Calling from the render loop is safe.

crates/notedeck/src/tor.rs (8)

1-35: LGTM! Well-structured imports and constants with appropriate documentation.

The use of std::sync::mpsc with try_recv() in the poll method is a good choice for non-blocking status checks from the UI thread. The ReadyState enum cleanly encapsulates bootstrap completion signaling.


58-94: LGTM! Shutdown timeout properly implemented.

The stop() method correctly handles the shutdown timeout case, and the Drop implementation ensures cleanup. The abandoned thread will eventually terminate when the Arti runtime shuts down, which is an acceptable tradeoff to prevent blocking the main thread indefinitely.


96-125: LGTM! Clean type definitions with appropriate encapsulation.

TorStatus is Clone + Debug for easy UI consumption, while TorManager correctly avoids Clone since it owns a thread handle. No Mutex usage aligns with the coding guidelines.


161-193: LGTM! Non-blocking poll implementation follows UI guidelines.

The poll() method correctly uses try_recv() for non-blocking status checks, which aligns with the "never block the render loop" coding guideline. All channel states (Ok, Err, Empty, Disconnected) are properly handled with appropriate status transitions and cleanup.


285-301: Port verification before ready signal is well implemented.

The tokio::select! with biased correctly prioritizes proxy errors, and the port verification addresses the past review concern about premature ready signaling.

Consider: The 100ms delay before verification may be tight on heavily loaded systems. If flaky startup reports emerge, consider adding a small retry loop or increasing the delay.


360-401: LGTM! Minimal wasm32 stub with compatible API surface.

The stub correctly returns Unsupported status and is_supported() = false, allowing calling code to handle platform differences gracefully.


657-658: LGTM! Clean public API re-exports.

The consistent API surface across all platform implementations allows consuming code to work uniformly without platform-specific conditionals.


336-345: Clarify whether stream isolation is required.

The build_client_config() function configures only storage directories and uses Arti's default stream isolation behavior, which allows circuits to be shared across streams. If the PR objectives or security requirements call for stream isolation (to prevent shared circuits and relay correlation), explicit configuration is needed: either TorClient::isolated_client() or StreamPrefs with an IsolationToken. Confirm whether this is a requirement before implementing.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +297 to +301
let status_code: u16 = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(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

Status code parse failure returns 0, which is not handled.

If the status line is malformed, unwrap_or(0) sets status_code to 0. This value doesn't match the redirect check (300..400).contains(&status_code) but could cause unexpected behavior. Consider returning an error for malformed responses.

Proposed fix
     let status_code: u16 = status_line
         .split_whitespace()
         .nth(1)
         .and_then(|s| s.parse().ok())
-        .unwrap_or(0);
+        .ok_or_else(|| HyperHttpError::Uri)?; // Consider a dedicated MalformedResponse variant
📝 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
let status_code: u16 = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let status_code: u16 = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.ok_or_else(|| HyperHttpError::Uri)?;
🤖 Prompt for AI Agents
In `@crates/notedeck/src/media/network.rs` around lines 297 - 301, The status code
parsing silently falls back to 0 on malformed status lines (status_line ->
status_code) which bypasses the redirect check and can hide errors; update the
parser in network.rs to treat parse failures as an error instead of using
unwrap_or(0): attempt to extract and parse the status token from status_line,
and if missing or parse fails return/propagate an appropriate Err (or
Result::Err) from the surrounding function (rather than assigning 0), so callers
can handle malformed responses and the redirect check
((300..400).contains(&status_code)) won't operate on an invalid value.

Comment on lines +384 to +395
let nmethods = buf[1] as usize;
let mut methods = vec![0u8; nmethods];
stream
.read_exact(&mut methods)
.await
.map_err(|e| anyhow!("Failed to read methods: {}", e))?;

// Send response - no auth required
stream
.write_all(&[0x05, 0x00])
.await
.map_err(|e| anyhow!("Failed to send auth response: {}", e))?;
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

SOCKS5 authentication negotiation doesn't verify client supports no-auth.

The code reads the client's supported methods but doesn't verify that 0x00 (no authentication) is in the list before responding with [0x05, 0x00]. Per RFC 1928, if no acceptable method is found, the server should respond with 0xFF.

Proposed fix to validate auth method
     let nmethods = buf[1] as usize;
     let mut methods = vec![0u8; nmethods];
     stream
         .read_exact(&mut methods)
         .await
         .map_err(|e| anyhow!("Failed to read methods: {}", e))?;

-    // Send response - no auth required
-    stream
-        .write_all(&[0x05, 0x00])
-        .await
-        .map_err(|e| anyhow!("Failed to send auth response: {}", e))?;
+    // Verify client supports no-auth (0x00)
+    if !methods.contains(&0x00) {
+        stream.write_all(&[0x05, 0xFF]).await?;
+        return Err(anyhow!("Client doesn't support no-auth method"));
+    }
+
+    // Send response - no auth required
+    stream
+        .write_all(&[0x05, 0x00])
+        .await
+        .map_err(|e| anyhow!("Failed to send auth response: {}", e))?;
📝 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
let nmethods = buf[1] as usize;
let mut methods = vec![0u8; nmethods];
stream
.read_exact(&mut methods)
.await
.map_err(|e| anyhow!("Failed to read methods: {}", e))?;
// Send response - no auth required
stream
.write_all(&[0x05, 0x00])
.await
.map_err(|e| anyhow!("Failed to send auth response: {}", e))?;
let nmethods = buf[1] as usize;
let mut methods = vec![0u8; nmethods];
stream
.read_exact(&mut methods)
.await
.map_err(|e| anyhow!("Failed to read methods: {}", e))?;
// Verify client supports no-auth (0x00)
if !methods.contains(&0x00) {
stream.write_all(&[0x05, 0xFF]).await?;
return Err(anyhow!("Client doesn't support no-auth method"));
}
// Send response - no auth required
stream
.write_all(&[0x05, 0x00])
.await
.map_err(|e| anyhow!("Failed to send auth response: {}", e))?;

@alltheseas
Copy link
Copy Markdown
Contributor Author

Analysis of CodeRabbit Feedback:

  STALE/DISMISS (4 items)

  The vendor/ewebsock comments are all stale - that code was removed in commit 9e57ee5c Replace vendored ewebsock with git dependency. These can be ignored.

  OVERLY CAUTIOUS/NOT WORTH FIXING (4 items)

  1. TorManager.java port 9150 - On mobile devices, Tor Browser running simultaneously is extremely rare. Not worth dynamic port allocation complexity.
  2. settings.rs UI desync - Misunderstands the flow. The SettingsHandler only persists when set_use_tor() is called. On failure, we don't call it, so persisted state stays correct. UI may briefly show wrong but self-corrects.
  3. keyring dependency - Pre-existing dependency, not introduced by this PR. Out of scope.
  4. SOCKS5 auth negotiation - Academic correctness. All practical SOCKS clients support no-auth.

  WORTH CONSIDERING (3 items)

  1. network.rs comment - Valid but minor. Comment says "SOCKS proxy not yet supported" but reality is HTTP itself is unsupported on Android/WASM.
  2. build-arti.sh Apple Silicon - Valid. Missing darwin-aarch64 for M1/M2/M3 Macs.
  3. Status code parse - Low priority, would fail elsewhere anyway.

  CI FAILURE ANALYSIS

  The failures are NOT caused by our code:
  error[E0277]: the trait bound `AppID: Basic` is not satisfied
  --> ashpd-0.11.0/src/documents/mod.rs:391:16

  This is an upstream ashpd crate incompatibility with zbus on Linux - a pre-existing issue. Note that Check (android) and Test (macOS) both pass, confirming our code is fine.

  Recommendation: The CodeRabbit feedback is mostly noise. The CI failures are upstream dependency issues, not related to our Tor implementation.


alltheseas and others added 16 commits January 15, 2026 09:11
Add ewebsock dependency with SOCKS5 proxy support for routing
WebSocket connections through Tor. Uses git dependency pointing
to fork with transport abstraction layer.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Force bundled sqlite3 builds on Windows and other platforms to
ensure consistent behavior and avoid system library version issues.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Address CodeRabbit feedback for the Tor integration:

- Add shutdown timeout (5s) to prevent indefinite blocking when
  stopping the Tor runtime. Uses polling with is_finished() instead
  of blocking join() to allow timeout handling.

- Verify SOCKS port is actually listening before signaling ready.
  Previously the ready signal was sent after bootstrap but before
  confirming the proxy was accepting connections.

- Add comprehensive docstrings to all public types, methods, and
  helper functions explaining their purpose and behavior.

- Add documentation for the hardcoded port 9150, noting it may
  conflict with Tor Browser.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: alltheseas <[email protected]>
- Fix toggle desync: check Tor support before attempting toggle and only
  update settings if the operation succeeds
- Hide internal error details from users: log errors at debug level and
  show generic "Connection failed" message in UI
- Add clippy allow for too_many_arguments on process_settings_action

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: alltheseas <[email protected]>
Remove the `not(target_os = "android")` condition from the Transport
import and configure_transport method, allowing Android builds to
use custom transports for WebSocket connections. This is required
for routing WebSocket traffic through Tor's SOCKS proxy on Android.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add a global JNI cache that stores the JavaVM and ClassLoader for
cross-thread JNI access on Android. This is necessary because
FindClass() only works on the main thread, so we cache the
ClassLoader to enable JNI calls from background threads where
Tor operations run.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add build-tor-android.sh script that compiles the arti-mobile crate
for Android targets (arm64-v8a, armeabi-v7a, x86, x86_64). The script
creates the Cargo package structure and uses cargo-ndk to build
shared libraries that provide the native Tor implementation.

Supports Intel Mac, Apple Silicon, and Linux build hosts.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add Java classes that provide the Android-side Tor integration:
- TorProvider: Interface for Tor service abstraction
- ArtiTorProvider: Native implementation using Arti via JNI
- TorManager: Manages Tor lifecycle and provides SOCKS proxy info

These classes are called from Rust via JNI to bootstrap and control
the Tor connection on Android.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add Android-specific Tor implementation that communicates with the
Java TorManager class via JNI. The Rust TorManager wraps JNI calls
to start/stop Tor and retrieve SOCKS proxy information.

Also export jni_cache module from lib.rs for Android builds.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Update build configuration for Android Tor support:
- Makefile: Add android-tor target to build Arti native libraries
- build.gradle: Configure jniLibs directory for native libraries
- MainActivity: Initialize JNI cache and TorManager on startup

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Implement http_req_via_socks() function that performs HTTP requests
through a SOCKS5 proxy for Tor routing. Features:
- SOCKS5 connection via tokio-socks
- TLS support via tokio-rustls with webpki-roots
- HTTP/1.1 protocol with chunked transfer encoding
- Redirect handling (up to 5 redirects)
- Proper host header for virtual hosting

Add workspace dependencies: tokio-socks, tokio-rustls, webpki-roots

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add HttpConfig struct that carries SOCKS proxy configuration through
the media loading pipeline:
- StaticImgTexCache: Store and use HttpConfig for image fetching
- AnimatedImgTexCache: Store and use HttpConfig for GIF fetching
- TexturesCache: Propagate HttpConfig to both static and animated
- Images: Propagate HttpConfig through the cache hierarchy

This allows media fetching to be routed through Tor when enabled.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Update update_relay_transport() to also configure the HTTP proxy
for media loading when Tor state changes. When Tor is enabled,
both WebSocket connections and HTTP image fetches are routed
through the SOCKS proxy.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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: 6

🤖 Fix all issues with AI agents
In
`@crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java`:
- Around line 64-90: The start() method reads and writes the shared initialized
flag without synchronization which can race if called concurrently; make start()
thread-safe by synchronizing the critical section (or using a dedicated lock)
that checks initialized, creates cacheDir/stateDir, calls
provider.initialize(...), and sets initialized to true so directory creation and
provider.initialize(...) happen at most once; keep the outer logging and the
final call to provider.startSocksProxy(socksPort) outside or inside the
synchronized block as appropriate, but ensure the initialized check/assignment
and provider.initialize(...) are atomic.
- Around line 74-77: Check the boolean return values of cacheDir.mkdirs() and
stateDir.mkdirs() in TorManager and, if a call returns false and the directory
does not already exist, log a warning (including the directory absolute path and
that this may cause initialize() to fail) so failures due to permissions or IO
are visible; keep the existing flow otherwise (don’t change initialize()
behavior), and use the project’s logging mechanism (e.g., Log.w/TAG or existing
logger) to emit the warning.

In `@crates/notedeck_chrome/src/chrome.rs`:
- Around line 997-1008: The TorStatus::Failed branch currently injects the
internal Arti error (err.as_str()) into the user-facing tooltip inside the
TorIndicatorStyle created in the TorStatus::Failed arm; change this to a
user-friendly or summarized message instead of the raw error (e.g., use tr! with
a generic string like "Tor routing failed. Please check your settings or try
reconnecting" or map a few common err variants to short, non-sensitive
summaries) and update the tr! call used for tooltip so it no longer interpolates
err.as_str() but returns the safe message.

In `@crates/notedeck/src/media/network.rs`:
- Around line 384-388: The code currently treats a malformed chunk size as zero
by using unwrap_or(0) when parsing size_line into chunk_size, which silently
ends body reading; change the parsing to propagate an error instead of
defaulting to 0 (e.g., replace usize::from_str_radix(size_line.trim(),
16).unwrap_or(0) with a fallible parse that returns Err on failure or maps the
ParseIntError into the function's error type), and ensure the surrounding
function (the chunked-body reader that uses chunk_size and size_line) returns
that error instead of breaking/continuing.

In `@crates/notedeck/src/tor.rs`:
- Around line 513-515: The directory creation calls currently swallow errors
(let _ = std::fs::create_dir_all(&self.cache_dir); let _ =
std::fs::create_dir_all(&self.state_dir);), which can hide failures that cause
the subsequent JNI initialize() to fail; change these to propagate or log errors
instead: call std::fs::create_dir_all(...) and handle the Result (use ? to
return the error from the enclosing function or map_err to convert into your
crate's error type) for both self.cache_dir and self.state_dir, and only call
JNI initialize() after both creations succeed (or at minimum log the path and
error via your logger before proceeding).
- Around line 486-491: The code currently casts the JNI-returned port to u16
directly in get_socks_port() -> TorStatus::Running which can produce wrong
values for negative or >65535 ports; update the block around get_socks_port() so
you validate the returned port is within 1..=65535 (or 0 if you allow 0) before
converting, e.g., use an explicit range check or TryFrom<i32> to u16, and only
set self.status = TorStatus::Running { socks_port: port as u16 } when the check
passes; if the port is out of range, do not cast—handle it by setting an
appropriate non-running status (e.g., TorStatus::Stopped or TorStatus::Error)
and add a log/warning indicating the invalid JNI port value.
♻️ Duplicate comments (6)
crates/notedeck/Cargo.toml (1)

57-58: Keyring dependency remains ungated despite platform concerns.

The previous review flagged that keyring should be moved to a platform-specific section since it doesn't support WASM. The dependency is still in the main [dependencies] block. If WASM builds are planned, this will cause compilation failures.

Verify if WASM builds are actually targeted and if keyring needs platform gating:

#!/bin/bash
# Check if there's WASM-specific handling or if keyring usage is already gated in code
rg -n "target_arch.*wasm|cfg.*wasm" crates/notedeck/src --type rust | head -20

# Check keyring usage pattern
rg -n "use.*keyring|keyring::" crates/notedeck/src --type rust -B2 -A2 | head -30
tools/arti-build/src/lib.rs (1)

384-395: SOCKS5 authentication negotiation doesn't verify client supports no-auth.

The code reads the client's supported methods but doesn't verify that 0x00 (no authentication) is in the list before responding with [0x05, 0x00]. Per RFC 1928, if no acceptable method is found, the server should respond with 0xFF.

Proposed fix to validate auth method
     let nmethods = buf[1] as usize;
     let mut methods = vec![0u8; nmethods];
     stream
         .read_exact(&mut methods)
         .await
         .map_err(|e| anyhow!("Failed to read methods: {}", e))?;

+    // Verify client supports no-auth (0x00)
+    if !methods.contains(&0x00) {
+        stream.write_all(&[0x05, 0xFF]).await?;
+        return Err(anyhow!("Client doesn't support no-auth method"));
+    }
+
     // Send response - no auth required
     stream
         .write_all(&[0x05, 0x00])
         .await
         .map_err(|e| anyhow!("Failed to send auth response: {}", e))?;
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (1)

14-14: Fixed SOCKS port 9150 may collide with Tor Browser.

This was flagged in a previous review. Tor Browser uses port 9150 by default. If both run concurrently, startSocksProxy will fail or produce undefined behavior. Consider probing for an available port or reporting a clear error when the port is occupied.

crates/notedeck_columns/src/ui/settings.rs (1)

594-602: Potential UI desync: toggle_value mutates settings before action is processed.

The toggle_value widget (line 596) mutates self.settings.use_tor immediately when clicked. If tor.set_enabled() fails in the action handler (lines 110-115), the local Settings struct retains the toggled value even though the operation failed. The SettingsHandler won't persist it, but the UI will show the incorrect state until the view is reconstructed.

Consider using a checkbox that doesn't directly mutate the settings, or revert the settings value on failure:

🔧 Option A: Use checked state without direct mutation
-                if ui
-                    .toggle_value(
-                        &mut self.settings.use_tor,
-                        RichText::new(label).text_style(NotedeckTextStyle::Small.text_style()),
-                    )
-                    .changed()
-                {
-                    action = Some(SettingsAction::ToggleTor(self.settings.use_tor));
-                }
+                let mut checked = self.settings.use_tor;
+                if ui
+                    .toggle_value(
+                        &mut checked,
+                        RichText::new(label).text_style(NotedeckTextStyle::Small.text_style()),
+                    )
+                    .changed()
+                {
+                    // Don't mutate settings here - let the action handler do it
+                    action = Some(SettingsAction::ToggleTor(checked));
+                }
🔧 Option B: Revert in action handler on failure
             Self::ToggleTor(enabled) => {
                 if !notedeck::TorManager::is_supported() {
                     tracing::warn!("Tor toggle requested but not supported on this platform");
+                    settings.set_use_tor(!enabled); // Revert the toggle
                     return route_action;
                 }

                 match tor.set_enabled(enabled) {
                     Ok(()) => settings.set_use_tor(enabled),
                     Err(err) => {
                         tracing::error!("failed to toggle tor: {err}");
+                        settings.set_use_tor(!enabled); // Revert the toggle
                     }
                 }
             }
crates/notedeck/src/media/network.rs (2)

38-46: Misleading comment: HTTP is entirely unsupported on Android/WASM, not just SOCKS proxy.

The comment states "SOCKS proxy not yet supported for HTTP" but http_req returns HyperHttpError::Unsupported on these platforms, meaning all HTTP requests fail. Update the comment to accurately reflect that HTTP requests are not supported on Android/WASM platforms.

Suggested fix
 /// Stub for Android/WASM
 #[cfg(any(target_arch = "wasm32", target_os = "android"))]
 pub async fn http_fetch(
     url: &str,
     _socks_proxy: Option<&str>,
 ) -> Result<HyperHttpResponse, HyperHttpError> {
-    // On Android/WASM, SOCKS proxy not yet supported for HTTP
+    // HTTP requests not supported on Android/WASM platforms
     http_req(url).await
 }

297-301: Malformed status line silently falls back to 0, masking protocol errors.

If the status line is malformed or missing, status_code becomes 0, which bypasses the redirect check and proceeds to read the body as if the request succeeded. This could mask HTTP protocol errors.

Suggested fix
     let status_code: u16 = status_line
         .split_whitespace()
         .nth(1)
         .and_then(|s| s.parse().ok())
-        .unwrap_or(0);
+        .ok_or_else(|| HyperHttpError::Uri)?; // Or add a MalformedResponse variant
🧹 Nitpick comments (11)
tools/arti-build/src/lib.rs (2)

469-500: Bidirectional copy silently drops the losing task's result.

When one direction completes or errors, tokio::select! cancels the other branch without checking if an error occurred. If client_to_tor fails with an error, it's silently ignored when tor_to_client wins the race, or vice versa.

Consider using tokio::try_join! or handling both results
-    tokio::select! {
-        _ = client_to_tor => {}
-        _ = tor_to_client => {}
-    }
+    // Use select but log errors from the losing side
+    tokio::select! {
+        result = client_to_tor => {
+            if let Err(e) = result {
+                log_to_java(&format!("client_to_tor error: {}", e));
+            }
+        }
+        result = tor_to_client => {
+            if let Err(e) = result {
+                log_to_java(&format!("tor_to_client error: {}", e));
+            }
+        }
+    }

Alternatively, for a cleaner shutdown where both sides complete:

let _ = tokio::join!(client_to_tor, tor_to_client);

464-467: Success response reports dummy bind address instead of actual.

The SOCKS5 success response sends 127.0.0.1:0 as the bound address. While most clients ignore this for CONNECT commands, RFC 1928 expects the actual bound address. For a local proxy this is minor but could confuse strict clients.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorLogCallback.java (1)

6-12: Consider adding @FunctionalInterface annotation.

Since this interface has exactly one abstract method and is intended to be used as a callback (likely with lambdas), adding the @FunctionalInterface annotation would provide compile-time validation and clearer intent.

Suggested change
 /**
  * Callback interface for receiving Tor log messages.
  */
+@FunctionalInterface
 public interface TorLogCallback {
     /**
      * Called when a log message is received from the Tor client.
      *
      * `@param` message The log message
      */
     void onLog(String message);
 }
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorProvider.java (1)

7-57: LGTM! Well-designed provider interface.

The interface provides a clean abstraction for swapping between native Arti and stub implementations. The method signatures are clear and the Javadoc is comprehensive.

One optional improvement: consider adding Android nullability annotations (@Nullable for getSocksProxy() return value and @NonNull for string parameters) to improve null-safety for callers.

crates/notedeck/src/lib.rs (1)

16-16: Consider adding #[cfg(target_os = "android")] to the jni_cache module declaration.

The jni_cache module is Android-specific JNI code. While the module file already gates all real implementations behind #[cfg(target_os = "android")], moving this attribute to the module declaration in lib.rs would make the Android-only nature explicit at the crate level and prevent accidental imports of Android-specific functions on other platforms.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/ArtiNative.java (1)

41-82: Consider documenting the precondition for native method calls.

Callers must check isLibraryLoaded() before invoking native methods to avoid UnsatisfiedLinkError. While this is standard JNI practice, adding a class-level Javadoc note about this precondition would improve API clarity.

📝 Suggested documentation addition
 /**
  * JNI bindings to the Arti Tor client native library.
+ * 
+ * <p><b>Important:</b> Callers must verify {`@link` `#isLibraryLoaded`()} returns
+ * {`@code` true} before invoking any native methods, otherwise an
+ * {`@link` UnsatisfiedLinkError} will be thrown.</p>
  */
 public class ArtiNative {
crates/notedeck/src/media/static_imgs.rs (1)

25-29: LGTM! Clean HttpConfig definition.

The HttpConfig struct with Clone and Default derives is well-designed for thread-safe propagation. The socks_proxy field correctly uses Option<String> to support both direct and proxied connections.

Consider adding #[derive(Debug)] for easier logging/debugging of HTTP configuration state:

📝 Optional enhancement
 /// Configuration for HTTP requests, including optional SOCKS proxy.
-#[derive(Clone, Default)]
+#[derive(Clone, Default, Debug)]
 pub struct HttpConfig {
     pub socks_proxy: Option<String>,
 }
crates/notedeck/src/jni_cache.rs (1)

18-33: Global static JNI_CACHE may conflict with coding guidelines.

Per coding guidelines: "Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference."

However, for JNI initialization this pattern is likely necessary since:

  1. Java calls initClassLoader before Rust has control
  2. The ClassLoader must be available from any thread
  3. There's no practical way to pass this state through the call stack from Java

The Send + Sync implementations are correctly justified in the comments. If the team accepts this as a necessary exception for JNI interop, consider adding a comment explaining why this global is acceptable despite the guideline.

crates/notedeck/src/app.rs (1)

481-493: Consider clarifying the no-op install_crypto comment.

The comment "Crypto not needed on mobile/web platforms" may be misleading since Android still uses TLS for relay connections. Consider clarifying that the crypto provider is either handled differently on these platforms or that this specific initialization isn't required.

📝 Suggested comment improvement
 /// No-op on Android/WASM - crypto handled by platform or not needed
 #[cfg(any(target_arch = "wasm32", target_os = "android"))]
 pub fn install_crypto() {
-    // Crypto not needed on mobile/web platforms
+    // On Android, TLS is handled via native platform APIs or tokio-rustls
+    // picks up a default provider. On WASM, the browser handles TLS.
 }
crates/notedeck/src/media/network.rs (1)

216-267: Consider extracting redirect handling to reduce duplication.

The HTTPS (lines 219-240) and HTTP (lines 245-266) branches contain identical redirect handling logic. This could be refactored by extracting the redirect handling after do_http_request returns.

Suggested refactor
     loop {
         // Connect through SOCKS proxy
         let stream = Socks5Stream::connect(socks_proxy, (current_host.as_str(), current_port))
             .await
             .map_err(|e| HyperHttpError::Hyper(Box::new(e)))?;

         let mut stream = stream.into_inner();

+        let (content_type, bytes, redirect) = if current_is_https {
             // If HTTPS, wrap with TLS
-        if current_is_https {
             let mut root_store = rustls::RootCertStore::empty();
             root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
             // ... TLS setup ...
             let mut tls_stream = connector.connect(server_name, stream).await...;
-            let (content_type, bytes, redirect) =
-                do_http_request(&mut tls_stream, &current_host, &current_path).await?;
-
-            if let Some(location) = redirect {
-                // ... redirect handling ...
-            }
-            return Ok(HyperHttpResponse { content_type, bytes });
+            do_http_request(&mut tls_stream, &current_host, &current_path).await?
         } else {
-            let (content_type, bytes, redirect) =
-                do_http_request(&mut stream, &current_host, &current_path).await?;
-
-            if let Some(location) = redirect {
-                // ... identical redirect handling ...
-            }
-            return Ok(HyperHttpResponse { content_type, bytes });
+            do_http_request(&mut stream, &current_host, &current_path).await?
+        };
+
+        // Unified redirect handling
+        if let Some(location) = redirect {
+            if redirects >= MAX_REDIRECTS {
+                return Err(HyperHttpError::TooManyRedirects);
+            }
+            let (new_host, new_port, new_path, new_is_https) = parse_redirect_location(...)?;
+            // ... update state ...
+            redirects += 1;
+            continue;
         }
+        return Ok(HyperHttpResponse { content_type, bytes });
     }
crates/notedeck/src/tor.rs (1)

478-494: Consider adding a bootstrap timeout for the Starting state.

If the JNI getSocksPort() keeps returning 0 (e.g., bootstrap stalls), the status remains Starting indefinitely with no feedback to the user. Consider adding a timeout that transitions to Failed after a reasonable period.

Suggested approach

Track when Starting state began and transition to Failed after a timeout (e.g., 60-120 seconds):

pub struct TorManager {
    status: TorStatus,
    // ... existing fields ...
    /// Timestamp when Starting state began, for timeout detection
    starting_since: Option<std::time::Instant>,
}

pub fn poll(&mut self) {
    // ... existing checks ...
    
    if matches!(self.status, TorStatus::Starting) {
        // Check for timeout
        if let Some(started) = self.starting_since {
            if started.elapsed() > Duration::from_secs(120) {
                self.status = TorStatus::Failed("Tor bootstrap timed out".to_string());
                self.starting_since = None;
                return;
            }
        }
        
        if let Some(port) = self.get_socks_port() {
            if port > 0 {
                self.status = TorStatus::Running { socks_port: port as u16 };
                self.starting_since = None;
            }
        }
    }
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1326e0e and 05efa1e.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tools/arti-build/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • .cargo/config.toml
  • .gitattributes
  • .gitignore
  • Cargo.toml
  • Makefile
  • crates/enostr/src/relay/mod.rs
  • crates/enostr/src/relay/pool.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/app.rs
  • crates/notedeck/src/context.rs
  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/jni_cache.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/media/gif.rs
  • crates/notedeck/src/media/network.rs
  • crates/notedeck/src/media/static_imgs.rs
  • crates/notedeck/src/persist/settings_handler.rs
  • crates/notedeck/src/tor.rs
  • crates/notedeck_chrome/android/app/build.gradle
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/ArtiNative.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/NativeTorProvider.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/StubTorProvider.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorLogCallback.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorProvider.java
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • tools/arti-build/ARTI_VERSION
  • tools/arti-build/Cargo.toml
  • tools/arti-build/build-arti.sh
  • tools/arti-build/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • .gitattributes
🚧 Files skipped from review as they are similar to previous changes (7)
  • tools/arti-build/Cargo.toml
  • tools/arti-build/ARTI_VERSION
  • crates/notedeck/src/context.rs
  • crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/NativeTorProvider.java
  • crates/notedeck_chrome/android/app/build.gradle
  • tools/arti-build/build-arti.sh
  • .gitignore
🧰 Additional context used
📓 Path-based instructions (4)
Cargo.toml

📄 CodeRabbit inference engine (AGENTS.md)

Do not vendor code; in Cargo.toml replace the existing URL with the fork that includes the new code. If vendoring is absolutely necessary, present the case why no other options are feasible

Files:

  • Cargo.toml
crates/notedeck*/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

crates/notedeck*/src/**/*.rs: Use AppContext to access mutable handles to services (database, caches, relay pool, account state, localization, settings, wallet) rather than accessing global state
Never use Mutexes in UI paths; the render loop must never block. Use Rc<RefCell<>> for single-threaded interior mutability, Arc<tokio::sync::RwLock<>> for cross-thread sharing, and poll_promise::Promise for async results
Wrap async work in poll_promise::Promise, check with promise.ready() or promise.ready_mut() each frame—never block the render loop
Use tokio::sync::RwLock instead of Arc<Mutex<>> for cross-thread sharing in Notedeck
Persist state via TimedSerializer::try_save to avoid blocking the frame; batch mutations with SettingsHandler::update_batch
Wrap user-facing strings with tr! or tr_plural! macros for localization and run python3 scripts/export_source_strings.py after changing strings
Use JobPool for CPU-bound work and return results via tokio::sync::oneshot wrapped in Promises; use tokio::spawn() for network I/O and relay sync
Mark performance-critical functions with #[profiling::function] for visibility in the puffin profiler
Use tracing macros for structured logging and profiling scopes where hot paths exist
Prefer early returns and guard clauses over deeply nested conditionals; simplify control flow by exiting early instead of wrapping logic in multiple layers of if statements (Nevernesting principle)
Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference
Ensure docstring coverage for any code added or modified
Avoid Mutexes in Notedeck code; prefer poll_promise::Promise for async results, Rc<RefCell<>> for single-threaded interior mutability, or tokio::sync::RwLock when cross-thread sharing is truly necessary
Never block the render loop; use Promise::ready() for non-blocking result checks. Offload CPU-heavy work to...

Files:

  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/jni_cache.rs
  • crates/notedeck/src/media/gif.rs
  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/media/static_imgs.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck/src/media/network.rs
  • crates/notedeck/src/persist/settings_handler.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/tor.rs
crates/notedeck*/src/app.rs

📄 CodeRabbit inference engine (AGENTS.md)

Implement the App trait with update(&mut self, &mut AppContext, &mut egui::Ui) -> AppResponse to drive egui rendering and signal high-level actions

Files:

  • crates/notedeck/src/app.rs
crates/notedeck*/src/lib.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use strict Rust 2021 edition; ensure edition-lints are strict and clippy disallowed_methods is denied at crate root to enforce API hygiene

Files:

  • crates/notedeck/src/lib.rs
🧠 Learnings (18)
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to Cargo.toml : Do not vendor code; in Cargo.toml replace the existing URL with the fork that includes the new code. If vendoring is absolutely necessary, present the case why no other options are feasible

Applied to files:

  • Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : For animations (GIFs, video), track `repaint_at` timestamps and only request repaints when necessary; avoid spinning every frame

Applied to files:

  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/media/gif.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `JobPool` for CPU-bound work and return results via `tokio::sync::oneshot` wrapped in Promises; use `tokio::spawn()` for network I/O and relay sync

Applied to files:

  • crates/enostr/src/relay/pool.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Embrace egui idioms in UI code: builder chains, closures returning `Response`, `ui.vertical`/`horizontal` for layout

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Wrap user-facing strings with `tr!` or `tr_plural!` macros for localization and run `python3 scripts/export_source_strings.py` after changing strings

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `AppContext` to access mutable handles to services (database, caches, relay pool, account state, localization, settings, wallet) rather than accessing global state

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/lib.rs : Use strict Rust 2021 edition; ensure edition-lints are strict and clippy `disallowed_methods` is denied at crate root to enforce API hygiene

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `tokio::sync::RwLock` instead of `Arc<Mutex<>>` for cross-thread sharing in Notedeck

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/lib.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Avoid Mutexes in Notedeck code; prefer `poll_promise::Promise` for async results, `Rc<RefCell<>>` for single-threaded interior mutability, or `tokio::sync::RwLock` when cross-thread sharing is truly necessary

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Persist state via `TimedSerializer::try_save` to avoid blocking the frame; batch mutations with `SettingsHandler::update_batch`

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/persist/settings_handler.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/app.rs : Implement the `App` trait with `update(&mut self, &mut AppContext, &mut egui::Ui) -> AppResponse` to drive egui rendering and signal high-level actions

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Global variables are not allowed in this codebase, even if they are thread local; state should be managed in a struct that is passed in as reference

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Never use Mutexes in UI paths; the render loop must never block. Use `Rc<RefCell<>>` for single-threaded interior mutability, `Arc<tokio::sync::RwLock<>>` for cross-thread sharing, and `poll_promise::Promise` for async results

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck_chrome/src/chrome.rs
  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Use `tracing` macros for structured logging and `profiling` scopes where hot paths exist

Applied to files:

  • crates/notedeck_columns/src/ui/settings.rs
  • crates/notedeck/src/app.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Prefer module-level organization over monolithic files; each feature (accounts, decks, timelines, media) should live in its own module tree

Applied to files:

  • crates/notedeck/src/app.rs
  • crates/notedeck_columns/src/nav.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/lib.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Ensure docstring coverage for any code added or modified

Applied to files:

  • crates/notedeck/src/app.rs
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/tor.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Never block the render loop; use `Promise::ready()` for non-blocking result checks. Offload CPU-heavy work to `JobPool` or `tokio::spawn()`, returning results via channels or Promises

Applied to files:

  • crates/notedeck/src/app.rs
  • crates/notedeck/src/tor.rs
📚 Learning: 2026-01-05T20:25:35.921Z
Learnt from: CR
Repo: damus-io/notedeck PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-05T20:25:35.921Z
Learning: Applies to crates/notedeck*/src/**/*.rs : Tests should live alongside modules (e.g., in test submodules), often using `#[tokio::test]` when async behavior is involved

Applied to files:

  • crates/notedeck/Cargo.toml
🧬 Code graph analysis (8)
crates/notedeck/src/imgcache.rs (2)
crates/notedeck/src/media/gif.rs (1)
  • set_http_config (129-131)
crates/notedeck/src/media/static_imgs.rs (1)
  • set_http_config (47-49)
crates/notedeck/src/media/gif.rs (2)
crates/notedeck/src/imgcache.rs (5)
  • new (37-47)
  • new (114-139)
  • new (290-300)
  • set_http_config (50-53)
  • set_http_config (308-310)
crates/notedeck/src/media/static_imgs.rs (2)
  • new (38-44)
  • set_http_config (47-49)
crates/notedeck_columns/src/ui/settings.rs (2)
crates/notedeck/src/tor.rs (3)
  • is_supported (128-130)
  • is_supported (380-382)
  • is_supported (452-455)
crates/notedeck/src/ui.rs (1)
  • richtext_small (5-10)
crates/notedeck_chrome/src/chrome.rs (1)
crates/notedeck/src/tor.rs (3)
  • status (133-135)
  • status (384-386)
  • status (457-459)
crates/notedeck/src/media/static_imgs.rs (2)
crates/notedeck/src/imgcache.rs (5)
  • new (37-47)
  • new (114-139)
  • new (290-300)
  • set_http_config (50-53)
  • set_http_config (308-310)
crates/notedeck/src/media/gif.rs (2)
  • new (120-126)
  • set_http_config (129-131)
crates/notedeck/src/app.rs (1)
crates/notedeck/src/tor.rs (6)
  • is_supported (128-130)
  • is_supported (380-382)
  • is_supported (452-455)
  • socks_proxy (153-159)
  • socks_proxy (392-394)
  • socks_proxy (470-476)
crates/notedeck_columns/src/nav.rs (1)
crates/notedeck/src/tor.rs (3)
  • is_supported (128-130)
  • is_supported (380-382)
  • is_supported (452-455)
crates/notedeck/src/lib.rs (1)
crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (1)
  • TorManager (12-134)
🔇 Additional comments (65)
.cargo/config.toml (1)

4-6: [env] section applies globally, not just to aarch64-unknown-linux-gnu.

In TOML, [env] is a new top-level section that closes the previous [target.aarch64-unknown-linux-gnu] block. These environment variables will apply to all cargo builds, not only cross-compilation for that target.

If this is intentional (to ensure consistent SQLite bundling across all platforms), please disregard. If target-specific behavior was intended, note that Cargo does not support [target.<triple>.env]—you would need to set these variables externally when invoking the cross-compilation command, or use a build script with conditional logic.

crates/enostr/src/relay/mod.rs (4)

151-157: LGTM!

The addition of the private options field to store websocket configuration is appropriate. Keeping it private with access via update_options maintains proper encapsulation.


184-200: LGTM!

The updated Relay::new correctly accepts and stores the options, cloning when passing to connect_with_wakeup since the method takes ownership. Storing the original options enables consistent reconnection behavior.


218-225: LGTM!

Using self.options.clone() in connect ensures that reconnection attempts preserve the configured transport (e.g., SOCKS proxy for Tor). This is essential for maintaining consistent routing behavior across disconnects.


231-234: LGTM!

The update_options method provides a clean way to update the stored options. The separation of concerns (updating options vs. triggering reconnection) is appropriate, with the caller responsible for reconnection when needed.

crates/enostr/src/relay/pool.rs (4)

10-18: LGTM!

The imports are appropriately organized with Transport and Arc gated behind #[cfg(not(target_arch = "wasm32"))] since SOCKS proxy transport is only available on native platforms.


128-149: LGTM!

The ws_options field is properly added to RelayPool and initialized to Options::default(), ensuring new pools start with direct connections until explicitly configured otherwise.


322-326: LGTM!

Passing self.ws_options.clone() to Relay::new ensures that newly added relays inherit the current transport configuration (e.g., Tor proxy if enabled).


350-366: Implementation correctly reconnects all Websocket relays on transport change.

This addresses the concern about existing relay sockets remaining direct after enabling Tor—only Websocket relays are reconnected, which is correct since Multicast relays use UDP multicast and don't require transport configuration. The options are updated before reconnect, so if reconnection fails, subsequent automatic reconnect attempts (via keepalive_ping) will use the new transport. The error is logged, which is appropriate.

Likely an incorrect or invalid review comment.

Cargo.toml (2)

140-141: Git-based dependency correctly replaces vendored code.

The ewebsock dependency now uses a git fork URL with a specific revision, addressing the previous review concern about vendoring. This aligns with the coding guidelines.


106-110: Arti dependencies properly configured with consistent pinned revision.

All five Arti-related crates are sourced from the official torproject GitLab repository and pinned to the same commit revision (624cdc9ec220124692c369f96ecb3cc72d9bbb66), ensuring reproducible builds. The required features (experimental-api, tokio, native-tls) are appropriately enabled. The configuration follows the guideline of using git URLs instead of vendoring code.

Makefile (2)

13-23: Well-structured build targets for Arti.

The new Makefile targets provide clear workflows for building Arti on Android:

  • arti: Full build for all architectures
  • arti-arm64: ARM64-only release build for faster development iteration
  • arti-clean: Cleanup target

The structure is clean and the comments are helpful.


46-50: Android Tor target properly integrates with build pipeline.

The android-tor target correctly:

  • Depends on arti-arm64 to ensure the native library is built
  • Adds appropriate logcat filters (NativeTorProvider, TorManager) for debugging Tor-related issues
crates/notedeck/Cargo.toml (1)

59-73: Arti/TLS dependencies correctly gated for desktop platforms.

The target-specific section properly excludes WASM and Android from Arti-related dependencies, which aligns with the PR notes that Android and WASM use stub modules instead of the native Arti runtime.

tools/arti-build/src/lib.rs (3)

1-6: Good module documentation.

Clear documentation explaining the purpose of the crate and its functionality.


25-56: Global mutable state with Mutex is acceptable for this JNI bridge.

While Notedeck crates prefer avoiding Mutexes, this is a standalone Android JNI library (tools/arti-build) that bridges Java and Rust. The global state pattern is common for JNI libraries where the Java side controls lifecycle. The use of OnceLock<Mutex<>> provides thread-safe initialization.


116-138: block_on will block the JNI calling thread—ensure this is never called from Android's main/UI thread.

The runtime.block_on() call on line 119 blocks whatever thread invokes the JNI method. If ArtiNative.initialize() is called from the main/UI thread, it will cause ANR (Application Not Responding) during network-dependent Tor bootstrap.

Currently, no active call sites to TorManager.start() or NativeTorProvider.initialize() are visible in the Java codebase. If these are used, they must be invoked from a background thread (e.g., via Thread, Executor, or coroutines). If this initialization is triggered from Rust or through undocumented code paths, ensure the calling context is a worker thread, not the main thread.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java (2)

34-36: Native method declaration for JNI classloader cache.

The initClassLoader native method enables Rust code to cache the Java ClassLoader for later JNI calls. This is a standard pattern for JNI code that needs to find classes from native threads.


166-175: Correct initialization sequence for JNI classloader.

The implementation correctly:

  1. Calls initClassLoader after super.onCreate() which loads the native library
  2. Wraps in try/catch to handle cases where the native method isn't available
  3. Logs success/failure for debugging

The comment clearly explains the timing requirements.

crates/notedeck/src/persist/settings_handler.rs (2)

44-45: Consistent serde(default) usage for backward compatibility.

Using #[serde(default)] ensures that existing settings files without the use_tor field will deserialize correctly with the default value of false. This is the correct approach for adding new optional settings.


218-228: LGTM! Methods follow established patterns.

The set_use_tor and use_tor methods are consistent with other settings accessors in this file. The persistence via try_save correctly avoids blocking the render loop per coding guidelines.

crates/notedeck/src/imgcache.rs (2)

48-53: LGTM! Clean HTTP config propagation.

The method correctly propagates the HTTP configuration to both static and animated image caches. Cloning for static_image and moving for animated is an efficient pattern. The docstring adequately describes the purpose.


307-310: LGTM! Consistent delegation to textures cache.

The Images::set_http_config method properly delegates to TexturesCache, maintaining a clean layered architecture for HTTP config propagation.

crates/notedeck/src/lib.rs (3)

40-40: Good module organization for Tor feature.

Adding tor as a dedicated public module aligns with the coding guideline to prefer module-level organization where each feature lives in its own module tree.


68-70: LGTM! Appropriate public re-exports.

Re-exporting HttpConfig from static_imgs at the crate root provides convenient access for consumers configuring HTTP/SOCKS proxy settings for media fetching.


94-94: LGTM! Clean public API surface for Tor.

Re-exporting TorManager and TorStatus at crate root makes the Tor API easily accessible to dependent crates like notedeck_chrome and notedeck_columns.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/StubTorProvider.java (1)

1-48: LGTM! Clean stub implementation.

The stub provider correctly implements the TorProvider interface with appropriate no-op behavior and return values (false for operations, null for proxy address). This provides a safe fallback for builds without native Tor support.

crates/notedeck_columns/src/nav.rs (3)

42-43: LGTM! Proper Tor context wiring through navigation.

The TorManager import and usage correctly follows the AppContext pattern for accessing services. The ctx.tor reference is appropriately passed through the navigation layer.


711-719: LGTM! SettingsView correctly receives Tor state.

The tor.status() and TorManager::is_supported() calls are non-blocking and appropriately pass Tor state to the settings UI for rendering the toggle and status indicators.


589-597: LGTM! Settings action processing wired for Tor control.

The ctx.tor parameter enables the settings action handler to control Tor state (enable/disable toggle), following the established AppContext service access pattern.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/ArtiNative.java (1)

10-18: Good error handling for library loading.

The static initializer gracefully catches UnsatisfiedLinkError and stores the error message for diagnostic purposes, preventing crashes on devices without native support.

crates/notedeck/src/media/gif.rs (2)

113-131: LGTM! Consistent HttpConfig integration.

The http_config field and set_http_config setter mirror the pattern in StaticImgTexCache, maintaining consistency across image cache types. The default initialization ensures backward compatibility.


176-191: LGTM! Proper HttpConfig propagation to async fetch.

The http_config is correctly cloned before being moved into the async task, and socks_proxy.as_deref() properly converts Option<String> to Option<&str> for the http_fetch call. Error handling is appropriately in place.

Also applies to: 216-231

crates/notedeck/src/media/static_imgs.rs (2)

31-49: LGTM! Proper cache integration with HttpConfig.

The http_config field with its setter follows the pattern established across image caches and includes appropriate docstring coverage as per coding guidelines.


146-161: LGTM! Network fetch properly uses HttpConfig.

The http_fetch integration with socks_proxy.as_deref() correctly enables SOCKS proxy support for static image loading. Error handling is consistent with the animated image fetch path.

crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java (1)

36-47: LGTM on singleton accessor and passthrough methods.

The singleton pattern with synchronized getInstance() and the delegation methods (stop(), getSocksProxy(), isSupported(), isRunning(), setLogCallback()) are well-documented and correctly delegate to the underlying provider.

Also applies to: 92-134

crates/notedeck_chrome/src/chrome.rs (6)

75-80: LGTM on Tor palette constants.

The Tor brand color constants are well-defined and provide semantic naming for the indicator states.


920-930: LGTM on style data structures.

TorIndicatorStyle and OverlayStyle cleanly encapsulate per-status visual properties.


932-945: LGTM on blend_colors helper.

Correctly implements linear interpolation with proper clamping for all RGBA channels.


947-971: Floating Tor indicator implementation looks good.

The badge correctly anchors to the top-right, uses Order::Tooltip for proper layering, and provides hover feedback via tooltip. Per PR comments, you may want to consider adding click affordance in the future (e.g., click to retry on failure or show details), but the current hover-tooltip approach is functional.


1024-1079: LGTM on paint_tor_onion rendering.

The onion icon is rendered with proper layering: filled base, concentric accent strokes, sprout lines, and conditional slash overlay. The calculations are correct and the rendering approach is clean.


278-280: LGTM on Tor indicator integration.

The indicator is correctly placed after app actions are processed, queries status via a non-blocking clone, and passes localization context for tooltips.

crates/notedeck_columns/src/ui/settings.rs (5)

8-11: LGTM on imports and ToggleTor action variant.

Clean additions following the existing SettingsAction pattern.

Also applies to: 38-38


101-117: Correct error handling in ToggleTor action.

The handler properly checks is_supported() before toggling, only updates persistent settings on success, and logs errors. This addresses the UI desync concern from the previous review.


123-165: LGTM on SettingsView struct additions.

The tor_status and tor_supported fields are correctly added and initialized via the constructor.


604-636: LGTM on status label display with localization.

All status variants correctly use tr!() for localization. The Failed case now logs the error detail and shows a user-friendly message, addressing the previous review comment.

Per PR comments, consider adding guidance for Android users when Unsupported (e.g., "Consider using Orbot for Tor support"), but this is optional.


823-828: LGTM on network_section integration in UI flow.

The network section is correctly integrated between storage and keys sections, following the existing action propagation pattern.

crates/notedeck/src/jni_cache.rs (4)

1-16: LGTM on module documentation and imports.

The docstring clearly explains the Android ClassLoader problem this module solves, and imports are correctly cfg-gated for Android-only compilation.


35-86: LGTM on initClassLoader JNI entry point.

The function correctly handles:

  • Re-initialization guard (line 48)
  • Null classloader validation (line 53)
  • JavaVM extraction with error handling
  • GlobalRef creation to prevent garbage collection
  • OnceLock race condition handling (lines 81-85)

Good defensive programming with comprehensive error logging.


101-148: LGTM on find_class implementation.

Correctly implements cross-thread class loading:

  • Class name format conversion (lines 111-112)
  • Defensive exception clearing before/after JNI calls
  • Proper local reference creation from GlobalRef for thread safety
  • Clear error messages with class name context

The SAFETY comment at line 146 adequately justifies the JObject -> JClass conversion.


150-177: LGTM on with_jni and non-Android stubs.

with_jni correctly attaches the current thread to the JVM before executing the closure. The non-Android stubs appropriately no-op, with is_initialized() returning true since JNI isn't needed on those platforms.

crates/notedeck/src/app.rs (5)

10-11: LGTM on Tor-related imports and struct fields.

Clean additions following existing patterns. The ws_transport field is correctly cfg-gated for non-wasm32 platforms, and no Mutexes are introduced.

Also applies to: 30-33, 89-91


230-241: LGTM on TorManager initialization.

Correct initialization sequence:

  1. Load settings first
  2. Create TorManager with data path
  3. Only enable if both supported and configured
  4. Revert settings on failure to prevent desync
  5. Start with WsTransport::Direct until Tor is ready

143-145: LGTM on frame update integration.

tor.poll() is correctly called each frame to check async status changes. update_relay_transport() is guarded by change detection to avoid unnecessary work.


423-423: LGTM on tor reference in AppContext.

Follows the established pattern for service access via context, per coding guidelines.


453-478: Relay transport reconfiguration correctly reconnects existing sockets.

The pool.configure_transport() method iterates through all WebSocket relays, updates their transport options, and explicitly reconnects them via relay.connect(). Existing connections will use the new transport (Socks or Direct) immediately when reconfigured, not on next reconnect. The implementation is correct.

crates/notedeck/src/media/network.rs (4)

1-24: LGTM!

Platform-conditional imports are correctly gated, and the 20MB body limit is reasonable for media content.


48-154: LGTM!

The desktop HTTP implementation properly handles HTTPS with certificate fallback, redirect following with limits, and body size enforcement both from headers and during streaming.


412-449: LGTM!

Redirect location parsing correctly handles absolute URLs, relative paths, and query string preservation.


460-504: LGTM!

Error enum is well-structured with clear Display messages, and the Unsupported variant properly indicates platform limitations.

crates/notedeck/src/tor.rs (5)

20-28: LGTM!

Constants are well-documented with clear explanation of port 9150 choice and potential conflict with Tor Browser. The shutdown timeout addresses the blocking concern from PR comments.


58-94: LGTM!

The shutdown mechanism properly addresses the indefinite blocking concern with a timeout, and the Drop implementation ensures cleanup.


161-193: LGTM!

The poll() method correctly uses try_recv() for non-blocking status checks, adhering to the coding guideline of never blocking the render loop.


285-301: LGTM!

The port verification logic addresses the past review comment about premature ready signaling. The proxy is given time to bind, verified to be accepting connections, and only then is the ready signal sent.


360-401: LGTM!

The WASM stub correctly indicates Tor is unsupported on this platform with appropriate no-op implementations.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +64 to +90
public boolean start(int socksPort) {
if (!provider.isSupported()) {
Log.w(TAG, "Tor is not supported on this build");
return false;
}

if (!initialized) {
File cacheDir = new File(context.getCacheDir(), "tor");
File stateDir = new File(context.getFilesDir(), "tor");

// Create directories
cacheDir.mkdirs();
stateDir.mkdirs();

Log.i(TAG, "Initializing Tor (cache: " + cacheDir + ", state: " + stateDir + ")");

if (!provider.initialize(cacheDir.getAbsolutePath(), stateDir.getAbsolutePath())) {
Log.e(TAG, "Failed to initialize Tor");
return false;
}

initialized = true;
}

Log.i(TAG, "Starting SOCKS proxy on port " + socksPort);
return provider.startSocksProxy(socksPort);
}
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

start() method is not thread-safe.

While getInstance() is synchronized, start() is not. If called concurrently from multiple threads, the initialized flag check (line 70) and assignment (line 85) could race, potentially causing double initialization. Consider synchronizing start() or using a lock.

🔧 Suggested fix
-    public boolean start(int socksPort) {
+    public synchronized boolean start(int socksPort) {
📝 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
public boolean start(int socksPort) {
if (!provider.isSupported()) {
Log.w(TAG, "Tor is not supported on this build");
return false;
}
if (!initialized) {
File cacheDir = new File(context.getCacheDir(), "tor");
File stateDir = new File(context.getFilesDir(), "tor");
// Create directories
cacheDir.mkdirs();
stateDir.mkdirs();
Log.i(TAG, "Initializing Tor (cache: " + cacheDir + ", state: " + stateDir + ")");
if (!provider.initialize(cacheDir.getAbsolutePath(), stateDir.getAbsolutePath())) {
Log.e(TAG, "Failed to initialize Tor");
return false;
}
initialized = true;
}
Log.i(TAG, "Starting SOCKS proxy on port " + socksPort);
return provider.startSocksProxy(socksPort);
}
public synchronized boolean start(int socksPort) {
if (!provider.isSupported()) {
Log.w(TAG, "Tor is not supported on this build");
return false;
}
if (!initialized) {
File cacheDir = new File(context.getCacheDir(), "tor");
File stateDir = new File(context.getFilesDir(), "tor");
// Create directories
cacheDir.mkdirs();
stateDir.mkdirs();
Log.i(TAG, "Initializing Tor (cache: " + cacheDir + ", state: " + stateDir + ")");
if (!provider.initialize(cacheDir.getAbsolutePath(), stateDir.getAbsolutePath())) {
Log.e(TAG, "Failed to initialize Tor");
return false;
}
initialized = true;
}
Log.i(TAG, "Starting SOCKS proxy on port " + socksPort);
return provider.startSocksProxy(socksPort);
}
🤖 Prompt for AI Agents
In
`@crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java`
around lines 64 - 90, The start() method reads and writes the shared initialized
flag without synchronization which can race if called concurrently; make start()
thread-safe by synchronizing the critical section (or using a dedicated lock)
that checks initialized, creates cacheDir/stateDir, calls
provider.initialize(...), and sets initialized to true so directory creation and
provider.initialize(...) happen at most once; keep the outer logging and the
final call to provider.startSocksProxy(socksPort) outside or inside the
synchronized block as appropriate, but ensure the initialized check/assignment
and provider.initialize(...) are atomic.

Comment on lines +74 to +77
// Create directories
cacheDir.mkdirs();
stateDir.mkdirs();

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

Unchecked mkdirs() return values.

The mkdirs() calls don't check their return values. If directory creation fails (e.g., permissions issues), the subsequent initialize() call will fail with a potentially confusing error. Consider logging a warning if directory creation fails.

🔧 Suggested improvement
-            cacheDir.mkdirs();
-            stateDir.mkdirs();
+            if (!cacheDir.mkdirs() && !cacheDir.exists()) {
+                Log.w(TAG, "Failed to create cache directory: " + cacheDir);
+            }
+            if (!stateDir.mkdirs() && !stateDir.exists()) {
+                Log.w(TAG, "Failed to create state directory: " + stateDir);
+            }
🤖 Prompt for AI Agents
In
`@crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/tor/TorManager.java`
around lines 74 - 77, Check the boolean return values of cacheDir.mkdirs() and
stateDir.mkdirs() in TorManager and, if a call returns false and the directory
does not already exist, log a warning (including the directory absolute path and
that this may cause initialize() to fail) so failures due to permissions or IO
are visible; keep the existing flow otherwise (don’t change initialize()
behavior), and use the project’s logging mechanism (e.g., Log.w/TAG or existing
logger) to emit the warning.

Comment on lines +997 to +1008
TorStatus::Failed(err) => Some(TorIndicatorStyle {
base: TOR_ALERT,
accent: TOR_AMBER,
sprout: TOR_AMBER,
overlay: Some(OverlayStyle::Slash(Color32::from_rgb(250, 226, 226))),
tooltip: tr!(
i18n,
"Tor routing failed: {error}",
"Tooltip when Tor routing has failed",
error = err.as_str()
),
}),
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

Raw error still exposed in Failed tooltip.

The TorStatus::Failed(err) branch interpolates the internal error string into the user-facing tooltip (line 1006: error = err.as_str()). While the settings.rs file was updated to show a generic message, this tooltip still exposes internal Arti errors. Consider showing a user-friendly message here as well, or at least summarizing common errors.

🔧 Suggested approach
         TorStatus::Failed(err) => Some(TorIndicatorStyle {
             base: TOR_ALERT,
             accent: TOR_AMBER,
             sprout: TOR_AMBER,
             overlay: Some(OverlayStyle::Slash(Color32::from_rgb(250, 226, 226))),
-            tooltip: tr!(
-                i18n,
-                "Tor routing failed: {error}",
-                "Tooltip when Tor routing has failed",
-                error = err.as_str()
-            ),
+            tooltip: {
+                tracing::debug!("Tor routing failed: {err}");
+                tr!(
+                    i18n,
+                    "Tor routing failed. Check Settings for details.",
+                    "Tooltip when Tor routing has failed"
+                )
+            },
         }),
🤖 Prompt for AI Agents
In `@crates/notedeck_chrome/src/chrome.rs` around lines 997 - 1008, The
TorStatus::Failed branch currently injects the internal Arti error
(err.as_str()) into the user-facing tooltip inside the TorIndicatorStyle created
in the TorStatus::Failed arm; change this to a user-friendly or summarized
message instead of the raw error (e.g., use tr! with a generic string like "Tor
routing failed. Please check your settings or try reconnecting" or map a few
common err variants to short, non-sensitive summaries) and update the tr! call
used for tooltip so it no longer interpolates err.as_str() but returns the safe
message.

Comment on lines +384 to +388
let chunk_size = usize::from_str_radix(size_line.trim(), 16).unwrap_or(0);

if chunk_size == 0 {
break;
}
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

Malformed chunk size silently terminates body reading.

If the chunk size line contains invalid hex, unwrap_or(0) treats it as end-of-body, potentially truncating the response. Consider returning an error for malformed chunked encoding.

Suggested fix
-        let chunk_size = usize::from_str_radix(size_line.trim(), 16).unwrap_or(0);
+        let chunk_size = usize::from_str_radix(size_line.trim(), 16)
+            .map_err(|_| HyperHttpError::Uri)?; // Or add a MalformedChunk variant
 
         if chunk_size == 0 {
             break;
         }
📝 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
let chunk_size = usize::from_str_radix(size_line.trim(), 16).unwrap_or(0);
if chunk_size == 0 {
break;
}
let chunk_size = usize::from_str_radix(size_line.trim(), 16)
.map_err(|_| HyperHttpError::Uri)?;
if chunk_size == 0 {
break;
}
🤖 Prompt for AI Agents
In `@crates/notedeck/src/media/network.rs` around lines 384 - 388, The code
currently treats a malformed chunk size as zero by using unwrap_or(0) when
parsing size_line into chunk_size, which silently ends body reading; change the
parsing to propagate an error instead of defaulting to 0 (e.g., replace
usize::from_str_radix(size_line.trim(), 16).unwrap_or(0) with a fallible parse
that returns Err on failure or maps the ParseIntError into the function's error
type), and ensure the surrounding function (the chunked-body reader that uses
chunk_size and size_line) returns that error instead of breaking/continuing.

Comment on lines +486 to +491
if let Some(port) = self.get_socks_port() {
if port > 0 {
self.status = TorStatus::Running {
socks_port: port as u16,
};
}
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

Validate port range before casting to u16.

The port as u16 cast on line 489 can produce incorrect values if the JNI returns a negative number or a value > 65535. Consider adding range validation.

Suggested fix
                 if let Some(port) = self.get_socks_port() {
-                    if port > 0 {
+                    if port > 0 && port <= u16::MAX as i32 {
                         self.status = TorStatus::Running {
                             socks_port: port as u16,
                         };
                     }
                 }
🤖 Prompt for AI Agents
In `@crates/notedeck/src/tor.rs` around lines 486 - 491, The code currently casts
the JNI-returned port to u16 directly in get_socks_port() -> TorStatus::Running
which can produce wrong values for negative or >65535 ports; update the block
around get_socks_port() so you validate the returned port is within 1..=65535
(or 0 if you allow 0) before converting, e.g., use an explicit range check or
TryFrom<i32> to u16, and only set self.status = TorStatus::Running { socks_port:
port as u16 } when the check passes; if the port is out of range, do not
cast—handle it by setting an appropriate non-running status (e.g.,
TorStatus::Stopped or TorStatus::Error) and add a log/warning indicating the
invalid JNI port value.

Comment on lines +513 to +515
// Create directories
let _ = std::fs::create_dir_all(&self.cache_dir);
let _ = std::fs::create_dir_all(&self.state_dir);
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

Directory creation errors are silently ignored.

If directory creation fails, the subsequent JNI initialize() call may fail with a confusing error. Consider propagating the error or at least logging it.

Suggested fix
             // Create directories
-            let _ = std::fs::create_dir_all(&self.cache_dir);
-            let _ = std::fs::create_dir_all(&self.state_dir);
+            if let Err(e) = std::fs::create_dir_all(&self.cache_dir) {
+                tracing::warn!("Failed to create Tor cache directory: {}", e);
+            }
+            if let Err(e) = std::fs::create_dir_all(&self.state_dir) {
+                tracing::warn!("Failed to create Tor state directory: {}", e);
+            }
📝 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
// Create directories
let _ = std::fs::create_dir_all(&self.cache_dir);
let _ = std::fs::create_dir_all(&self.state_dir);
// Create directories
if let Err(e) = std::fs::create_dir_all(&self.cache_dir) {
tracing::warn!("Failed to create Tor cache directory: {}", e);
}
if let Err(e) = std::fs::create_dir_all(&self.state_dir) {
tracing::warn!("Failed to create Tor state directory: {}", e);
}
🤖 Prompt for AI Agents
In `@crates/notedeck/src/tor.rs` around lines 513 - 515, The directory creation
calls currently swallow errors (let _ =
std::fs::create_dir_all(&self.cache_dir); let _ =
std::fs::create_dir_all(&self.state_dir);), which can hide failures that cause
the subsequent JNI initialize() to fail; change these to propagate or log errors
instead: call std::fs::create_dir_all(...) and handle the Result (use ? to
return the error from the enclosing function or map_err to convert into your
crate's error type) for both self.cache_dir and self.state_dir, and only call
JNI initialize() after both creations succeed (or at minimum log the path and
error via your logger before proceeding).

alltheseas and others added 5 commits January 15, 2026 09:22
Move keyring back to the platform-gated dependencies section to
prevent WASM and Android build failures. The keyring crate doesn't
support these platforms.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Change "relay traffic" to "network traffic" since we route both
WebSocket relay connections and HTTP media fetches through Tor.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Instead of hardcoding port 9150 (which conflicts with Tor Browser),
automatically probe for an available port starting from 9150. This
avoids silent failures when another application is using the port.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add comprehensive test coverage for:
- TorManager state machine (Disabled -> Starting -> Running -> Disabled)
- Enable/disable idempotency
- Platform-specific behavior (desktop vs WASM)
- HttpConfig for SOCKS proxy configuration
- StaticImgTexCache proxy configuration

Tests verify correct state transitions without requiring actual Tor
network connectivity, making them suitable for CI environments.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Address three critical issues in Tor implementation:

1. [P1] Fix traffic leak during Tor bootstrap:
   - Add `should_allow_connections()` method to TorManager
   - Block relay connections during Starting state
   - Skip accounts.update() when Tor is bootstrapping
   - Prevents IP leaks before SOCKS proxy is ready
   (tor.rs:161-169, app.rs:147-151)

2. [P1] Fix runtime failure detection after bootstrap:
   - Monitor thread health in poll() after bootstrap completes
   - Detect when runtime thread exits unexpectedly
   - Update status to Failed if thread terminates
   (tor.rs:207-217)

3. [P2] Fix UI blocking during Tor shutdown:
   - Make stop() non-blocking by spawning cleanup thread
   - Send shutdown signal immediately without waiting
   - Background thread handles join with timeout
   - Prevents 5s UI freeze when disabling Tor
   (tor.rs:58-92)

Add tests for new should_allow_connections() behavior.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@alltheseas
Copy link
Copy Markdown
Contributor Author


⏺ All CodeRabbit feedback is resolved. The click navigation to settings is also in place (lines 278-280).

  Summary of all verified fixes:
  ┌──────────────────────┬────────────────────────────────────┬───────────────────────────────────────────────┐
  │       Category       │               Issue                │                  Resolution                   │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ Java Thread Safety   │ start() not thread-safe            │ Both methods have synchronized                │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ Java Directories     │ mkdirs() ignoring failures         │ Check return values, log warnings             │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ Java Port Collision  │ Hardcoded port 9150                │ Auto-detect available port starting from 9150 │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ Rust Error Handling  │ Silent chunk truncation            │ Return MalformedChunk error                   │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ Rust Port Validation │ Invalid port cast to u16           │ Validate 1-65535 before cast                  │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ Rust Directories     │ Silent directory creation failures │ Log warnings on failure                       │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ UI Tooltip           │ Raw error in tooltip               │ User-friendly message with call to action     │
  ├──────────────────────┼────────────────────────────────────┼───────────────────────────────────────────────┤
  │ UI Interaction       │ Icon not clickable                 │ Clicking navigates to Tor settings            │
  └──────────────────────┴────────────────────────────────────┴───────────────────────────────────────────────┘


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant