Follow-up to #3186, addressing the unresolved review feedback:
- RelayHealthStore.schedulePersist() wrapped the blocking save() in withContext(ioDispatcher)
so prefs.flush() no longer sits on the Compose composition thread on Desktop.
- close() now fires the final save on a detached IO-bound scope instead of blocking
the composition thread for ~50ms during account switch / app exit.
- @Volatile on persistJob/tickJob and a closed-flag guard so the relay-network thread
and composition thread no longer race on plain vars (and post-close work is dropped).
- desktopApp/Main.kt passes Dispatchers.IO to RelayHealthStore so persistence flushes
land on the IO dispatcher instead of Dispatchers.Default.
Plus a separate-but-related fix to the offline-banner-stuck-after-Mac-sleep issue:
NostrClient.keepAliveJob now tracks wall-clock overshoot of its scheduled tick.
If the OS suspended us (laptop lid closed, system sleep), delay() returns far
past its deadline and the OkHttp websockets we held are dead even though
BasicRelayClient.isConnected() still reads true until the next ping fails.
On a >5x interval overshoot, force relayPool.disconnect() + connect() instead
of trusting needsToReconnect(), so feeds resume without an app restart.
A relay that accepts the WebSocket handshake and then immediately resets
the connection (e.g. essayist.decentnewsroom.com) defeated the exponential
reconnect backoff.
UrlParser.parseValidUrls filtered every detected URL through
isValidTopLevelDomain(), which requires the TLD's first character to be
an ASCII letter. IPv6 literal hosts are bracketed (e.g. [2001:db8::1])
and have no dotted TLD, so the whole bracketed host became the candidate
"TLD", starting with '[' and failing the check. As a result, valid
IPv6 URLs like http://[302:68d0:f0d5:b88d::bdb]/<hash> were dropped and
rendered as plain text instead of links.
The UrlDetector already validates the bracketed address as syntactically
correct IPv6, so accept bracketed hosts directly in isValidTopLevelDomain.
The "Updates all dependencies" commit (ece719445) bumped Kotlin to 2.4.0,
which removed the `DisableCacheInKotlinVersion.2_3_21` enum value. Rather
than bumping the guard, the whole `disableUiKitPrebuiltCache()` workaround
was deleted — which reintroduced the iOS test link failure on CI
Drop three lower-impact items from the Highlights list (still documented
in the body): audio visualizer (author notes minimal visibility), LaTeX
math (niche), and pinned DMs (minor QoL). Keeps the headline payment,
privacy, and new-feed features front and center.
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
Reviewed all merged PRs since the v1.11.0 release. Fixed the misleading
'new money operations' Tor line (the toggle already existed; what changed
is relay sockets now honor it). Added gaps the PR titles surfaced: NIP-14
group DM subjects (#3163), removal of wallet reordering drag-and-drop
(#3114), and diagnosable relay failure logs (#3197).
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
A file-level audit (not just commit subjects) surfaced major features hidden
under misleading commit messages: the new NIP-60 Cashu ecash wallet, music
tracks/playlists, the NIP-82 Software Apps directory, the live audio
visualizer, and multi-wallet management (NWC/CLINK-debit/Cashu).
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
Adds Hidden Words unblock, Share to DM, reply-to-zaps from notifications,
the Selected/Global notification split, the podcasts subsystem, desktop
unhealthy-relay review, the live zap-popup rails, and the commons KMP
migration scope.
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
Summarizes the 506 commits / 131 PRs merged since the v1.11.0 release:
CLINK payments, private posts/reactions via NIP-17, NIP-101e workouts,
LaTeX math rendering, NIP-89 app recommendations, pinned DMs, activity
cards, desktop image compression + new media player, and the Quartz
relay-server toolkit. User-facing sections are written for end users;
build/quartz/cli sections keep developer-level language.
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
OkHttp 5.4.0 expanded Interceptor.Chain from ~10 to ~40 members, breaking
the test's hand-rolled `object : Interceptor.Chain` fake. Replace it with a
java.lang.reflect.Proxy that implements only request()/proceed() and throws
for the rest, so it survives future interface growth without dozens of empty
stubs. A real OkHttpClient can't be used here because Platform init fails
outside Robolectric (android.util.Log.isLoggable is unavailable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Namecoin diagnostics card that Android renders below the
per-server test results to the desktop settings panel, so support
requests carry the same information on both platforms.
- last test timestamp + pass/fail tally
- host OS (name/version/arch) — desktop equivalent of Android's
Build.MANUFACTURER/MODEL row
- JVM name + version — desktop equivalent of Android's API level row
- distinct TLS versions observed during the test run
Also adds a 'Testing next server...' inline progress row matching the
Android section's behaviour when the test loop is mid-run.
Pure UI addition; no model, preference, or callback changes.
The single CHANGELOG.md had grown to ~7.9k lines. Move it to
docs/changelog/ with one file per release, named with zero-padded
minor/patch versions (e.g. v1.11.00.md) so plain file browsers sort
them correctly. Add a README.md index (newest first) and a TEMPLATE.md
for cutting new releases. The root CHANGELOG.md now points to the new
location.
The RelayStat.lastConnectAt / lastIncomingAt fields added in #3186 were
written on every connect and every incoming relay message (a TimeUtils.now()
call plus a volatile store on the Android hot path) but never read.
The notifications "Global" mode was split into a raw Global (every event
that p-tags the user) and a curated Selected mode. Existing users who had
selected the old curated-Global kept a persisted value that now
deserializes to the much-more-permissive raw Global, so they suddenly saw
everything.
Add a one-shot, per-account migration: on load, if the account hasn't been
stamped yet and its notification filter is Global, rewrite it to Selected
and stamp the account. saveToEncryptedStorage always stamps the account as
migrated, so brand-new accounts (and any deliberate raw-Global choice made
after the split) are never reverted.
Only the notifications key is touched; Global keeps its original meaning
for every other feed.