Disabling individual sub-passes (method/specialization/returntype,
method/marking/static) was not sufficient. The interaction between
multiple optimization passes causes IncompatibleClassChangeError in
kmp-tor's AsyncFs.of() at launch. Shrink (dead code removal) stays
ON for the size win; only optimize is disabled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The smoke test discovered that ProGuard's method/marking/static pass
converts kmp-tor's AsyncFs.of() from instance to static. The JVM
then throws IncompatibleClassChangeError at launch because callers
still use invokevirtual.
Also updates smoke-test-desktop.yml trigger: runs on any PR touching
desktopApp/ (not just build config files).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the intentional breakage from d0a6bbc96. The smoke test
proved it catches the crash: removing java.management from jlink
modules causes a fatal IncompatibleClassChangeError in kmp-tor
at launch (the module is needed for ManagementFactory + tor runtime).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit should FAIL the release-deb-launch CI job, proving the
smoke test works. Will be reverted in the next commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#2819 — v1.08.0 .deb crashed on Ubuntu with ManagementFactory
error because the build had no jlink modules() declaration. Current
main already has the fix; this PR adds CI to prevent regressions.
- Add compose.desktop.uiTestJUnit4 dependency
- DesktopLaunchSmokeTest: renders LoginScreen, asserts title text
- build.yml: xvfb for Linux leg so UI test runs on every PR
- smoke-test-desktop.yml: builds release .deb, installs it, launches
under xvfb, verifies process stays alive 10s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MacOsVlcDiscoverer.setPluginPath() called LibC.INSTANCE.setenv() via
JNA, but macOS 13+ uses versioned symbols (setenv$3b99ba0d) that JNA
can't resolve, breaking all video playback without system VLC.
Changes:
- Replace LibC.setenv with direct JNA Function.getFunction("c","setenv")
call that bypasses the problematic interface binding
- Store discoveredPluginPath for --plugin-path factory arg fallback
- Add -Dvlc.plugin.path JVM property as ultimate fallback
- Delete stale VLC plugin cache on macOS before factory creation
- Pass --plugin-path to audio factory too when env var fails
- Add jdk.unsupported module to jlink (VLCJ ByteBufferFactory needs
sun.misc.Unsafe for video frame buffer allocation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
linuxdeploy auto-walks every binary in the staged AppDir with ldd to
bundle their shared-library deps. That fights jpackage's self-contained
output on two axes and aborts the createReleaseAppImage task before any
AppImage is produced:
- The bundled JRE puts libjvm.so at usr/lib/runtime/lib/server/, while
its sibling libs (libmanagement.so, libawt_xawt.so, libfontmanager.so
and others in usr/lib/runtime/lib/) have RPATH=$ORIGIN. ldd cannot
resolve libjvm.so from those libs without help, so linuxdeploy errors:
ERROR: Could not find dependency: libjvm.so
ERROR: Failed to deploy dependencies for existing files
- The bundled libvlc plugins under usr/lib/app/resources/vlc/ are
UPX-compressed by the vlc-setup plugin. patchelf cannot read their
section headers, so linuxdeploy aborts:
ERROR: Call to patchelf failed:
patchelf: no section headers. The input file is probably a
statically linked, self-decompressing binary
- Even after working around libjvm.so via LD_LIBRARY_PATH, several VLC
libs have RUNPATH pointing at the wrong directory, so ldd fails on
libva.so.2 / libvlccore.so.9 next.
This is why v1.09.2 shipped .deb + .rpm from the linux leg but no
.AppImage and no linux .tar.gz from the linux-portable leg — the gradle
step failed on linuxdeploy, so the subsequent "Build portable archives"
step never ran.
Swap to appimagetool, which only embeds the AppDir as-is into a
SquashFS-backed, runtime-prepended AppImage and never touches the
contents. jpackage already bundles a complete, self-contained app —
we don't need linuxdeploy's dep-discovery. AppRun continues to handle
LD_LIBRARY_PATH at launch.
Verified locally on ubuntu-24.04: a fresh
:desktopApp:createReleaseAppImage now produces a valid 254 MB
Amethyst-1.09.2-x86_64.AppImage in ~30 s on top of an up-to-date
createReleaseDistributable.
Side changes:
- Workflow installs desktop-file-utils (appimagetool 1.9.0 calls
desktop-file-validate on the .desktop entry).
- BUILDING.md updated with the new local-dev fetch command.
- .gitignore replaces the linuxdeploy paths with appimagetool's.
The :ammolite module contained no production Kotlin/Java sources (just
a manifest, build.gradle, and proguard stubs) and no module in the
codebase imports com.vitorpamplona.ammolite.*.
Removes:
- ammolite/ directory (5 files)
- :ammolite project include in settings.gradle
- implementation project(':ammolite') from :amethyst
- androidTestImplementation project(':ammolite') from :benchmark
- :ammolite:testDebugUnitTest from CI workflow and pre-push hook
- -keep class com.vitorpamplona.ammolite.** rules from
:amethyst, :commons, and :desktopApp proguard files
- Stale references in CONTRIBUTING.md, CLAUDE.md, and the
gradle-expert skill dependency-graph doc
Small build-graph win: one fewer module to configure, compile, lint,
and spotless-check on every build, and one fewer unit-test target in
both CI and the local pre-push hook.
Compose Multiplatform 1.11.0 wired ProGuard 7.7.0 into the release
build for the first time. With optimize + shrink + obfuscate all on,
the desktop v1.09.1 DMG hit four distinct runtime failures:
A. Jackson's SerializationFeature.values()/valueOf() stripped, so
Class.getEnumConstants() returned null and ObjectMapper failed
to initialise (app didn't launch). Fixed in #2921.
B. fr.acinq.secp256k1.* JNI bridge classes renamed by obfuscate;
bundled libsecp256k1-jni.dylib could not FindClass the original
names, so KeyPair / sign / verify crashed on first use.
C. androidx.sqlite-bundled native declarations (nativeThreadSafeMode,
nativeOpen) shrunk away because no Kotlin caller referenced them
directly; libsqliteJni.dylib raised NoSuchMethodError on the first
EventStore query.
D. ProGuard's method/specialization/returntype optimize sub-pass
generated a synthetic okio bridge (Okio__OkioKt.buffer$<hash>)
whose declared return type was RealBufferedSource but whose body
returned the BufferedSource super-interface. The JVM verifier
rejected the bridge, killing every OkHttp connection.
The Android (mobile) module solved the same class of problems in
amethyst/proguard-rules.pro with a single global strategy:
-dontobfuscate
-keepnames class ** { *; }
-keep enum ** { *; }
+ per-library -keep rules for JNA / libsodium / libscrypt
+ first-party -keep com.vitorpamplona.**
This commit replays that strategy in desktopApp/compose-rules.pro
and drops the previous optimize.set(false)/obfuscate.set(false)
escape hatch in desktopApp/build.gradle.kts. Shrink and optimize
both stay on; only the one optimize sub-pass that produced invalid
okio bytecode (method/specialization/returntype) is disabled.
Additional desktop-only keep rules (Jackson, full JNA, VLCj,
SLF4J, OkHttp / Conscrypt / BouncyCastle / OpenJSSE / Graal
dontwarns) stay in place. Two libraries shipped only on desktop
also get explicit -keep rules so their native callbacks survive
shrink:
- androidx.sqlite.** + native <methods>; (nativeThreadSafeMode
was previously stripped even with -keepnames, because shrink
removes members the global rule doesn't cover).
- pt.davidafsilva.apple.** + native <methods>; for the macOS
Keychain JNI bridge.
Verified on macOS arm64 packageReleaseDmg:
- javap on the post-ProGuard jars confirms:
* fr.acinq.secp256k1.NativeSecp256k1 keeps its FQCN.
* SerializationFeature.values() / valueOf() present.
* androidx.sqlite.driver.bundled.BundledSQLiteDriverKt
retains native nativeThreadSafeMode() + nativeOpen().
* Okio__OkioKt only exposes buffer(Source): BufferedSource
and buffer(Sink): BufferedSink — no specialized
buffer$<hash> bridge.
- Bundled app launches and reaches VLC MediaPlayerFactory init
with zero VerifyError / NoSuchMethodError / UnsatisfiedLinkError
in the log.
The ImportFollowListDialog composable was already implemented but never
rendered. The File menu item set a boolean state that no observer
consumed.
Render the dialog from MainContent inside the CompositionLocalProvider
that supplies LocalNamecoinService, so Namecoin (.bit, d/, id/)
identifier resolution works in addition to npub/hex/NIP-05.
Also add a left-side launcher in both layouts so the feature is
discoverable without using the File menu:
- single-pane: NavigationRailItem (PersonAdd icon, 'Import' label)
- deck: IconButton in the DeckSidebar next to 'Add Column'
The desktop client already ships DesktopNamecoinPreferences, the
DesktopNamecoinNameService that consumes them, and the NamecoinSettingsSection
composable that's a port of the Android UI. The section just wasn't surfaced
in the desktop Settings screen.
This wires NamecoinSettingsSection into RelaySettingsScreen, between the Tor
section and the Developer / Relay sections. Preferences come from the
namecoinPreferences parameter that the deck container already passes in, and
fall back to LocalNamecoinPreferences when called from another caller.
User-visible behaviour: the .bit / d/ / id/ ElectrumX server settings (master
toggle, custom servers, defaults indicator, add/remove, reset) are now
reachable from desktop Settings and persist via java.util.prefs.Preferences.
The v1.09.1 desktop release (DMG/MSI/DEB/RPM) fails to launch on every
platform with:
Exception in thread "main" java.lang.ExceptionInInitializerError
at com.fasterxml.jackson.databind.ObjectMapper.<init>(ObjectMapper.java:700)
at com.vitorpamplona.amethyst.desktop.ui.deck.DeckState.<clinit>
Caused by: java.lang.NullPointerException:
Cannot read the array length because the return value of
"java.lang.Class.getEnumConstants()" is null
at com.fasterxml.jackson.databind.cfg.MapperConfig
.collectFeatureDefaults(MapperConfig.java:107)
Failed to launch JVM
Root cause: PR #2914 wired Compose 1.11.0's new ProGuard pass into the
release build but added only `-dontwarn` rules. ProGuard then optimized
Jackson's enum classes (`SerializationFeature`, `MapperFeature`, etc.) and
stripped their synthetic `values()` / `valueOf()` methods because nothing
in Amethyst's own bytecode calls them. Jackson does call them — but
reflectively, at `ObjectMapper` construction time, long after ProGuard
finished. The result is `Class.getEnumConstants()` returning null and the
JVM giving up.
The same regression silently broke VLC media playback (VLCj's
`ServiceLoader` lookup of `DiscoveryDirectoryProvider` impls printed
`ConfigDirConfigFileDiscoveryDirectoryProvider not found` because
ProGuard stripped both the META-INF/services file and the impl class).
Fix: add explicit `-keep` rules for the four reflection-heavy libraries
on the desktop classpath:
- Jackson (databind, core, annotations, module-kotlin): keep classes
intact and keep `values()`/`valueOf()` on every Jackson enum.
- JNA (com.sun.jna.\*\*): used by VLCj and kmp-tor — its `Structure`
field-order reflection requires field metadata to survive.
- VLCj (uk.co.caprica.vlcj.\*\*): `ServiceLoader`-based discovery providers
and reflective binding classes throughout.
- SLF4J: detected by Jackson via `Class.forName`.
Verified by building `:desktopApp:createReleaseDistributable` and
launching the resulting bundle on macOS arm64:
- No `ExceptionInInitializerError`
- `VLC: bundled discovery succeeded`
- `VLC: MediaPlayerFactory created successfully`
The same ProGuard pass runs on all four target formats (Dmg, Msi, Deb,
Rpm) so this single change fixes the regression on every desktop OS.
Compose Multiplatform 1.11.0 tightened its default ProGuard ruleset so
`proguardReleaseJars` now fails on unresolved references that 1.10.3
tolerated silently. The v1.09.0 release tag-build hit this on every
desktop leg (macOS DMG, Windows MSI, Linux DEB/RPM, Linux AppImage),
which is why only Android APKs and the macOS-arm64 / Linux Amy CLI
bundles made it onto the GitHub release.
Three groups of references were flagged:
- Jackson kotlin-module 2.21.x value-class converters
(`IntValueClassBoxConverter`, `ValueClassKeyDeserializer$*`, …)
invoke `MethodHandle.invokeExact` with polymorphic signatures
ProGuard cannot resolve against the abstract MethodHandle.
- okhttp's optional TLS adapters (`okhttp3.internal.graal.*`,
`BouncyCastle/Conscrypt/OpenJSSEPlatform`) reference classes from
runtime backends that aren't on the desktop classpath; okhttp falls
back to the JDK default in their absence.
- JNA's `com.sun.jna.internal.Cleaner` inner classes touch synthetic
`access$N` accessors that ProGuard's class-member analysis cannot
follow.
Add `desktopApp/compose-rules.pro` with `-dontwarn` rules for each
bucket and wire it through `compose.desktop.application.buildTypes
.release.proguard.configurationFiles`. Verified locally by re-running
`:desktopApp:proguardReleaseJars` (now BUILD SUCCESSFUL) and
`:desktopApp:packageReleaseDeb` (gets past ProGuard; only fails on the
sandbox missing `fakeroot`, which CI installs in the existing
"Install RPM tooling" step).
CRITICAL: Move NWC wallet secret from plaintext nwc_connection.txt to OS
keychain. The NWC secret is a private key that can authorize Lightning
payments — storing it in plaintext allowed any process to steal funds.
Security fixes:
- NWC secret stored in OS keychain as "nwc_<npub>" (per-account)
- accounts.json.enc is now the sole source of truth for cold boot
- Eliminate bunker_uri.txt, last_account.txt, nwc_connection.txt
- Legacy files deleted on first startup (one-time cleanup)
- logout(deleteKey=true) now removes account from accounts.json.enc
- Corrupted accounts.json.enc backed up as .corrupt.<timestamp>
Cold boot rewrite:
- loadSavedAccount() routes by SignerType from accounts.json.enc
- No longer reads stale bunker_uri.txt (fixes nsec→bunker confusion)
- No longer reads last_account.txt (uses activeNpub from metadata)
Multi-account improvements:
- NWC connections are per-account (switch account = switch wallet)
- Each account type (Internal/Remote/ViewOnly) loads correctly
- saveBunkerAccount() no longer writes to bunker_uri.txt
Updated 8 existing test files to use accountStorage instead of
writing legacy files directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The AppImage build inputs (AppRun, .desktop entry, icon, and the
CI-fetched linuxdeploy binary) are consumed only by desktopApp's
createReleaseAppImage task. Co-locating them under
desktopApp/packaging/appimage/ removes the `../` path escape from the
build script and keeps all desktop packaging assets inside the module.
https://claude.ai/code/session_0137ULcfJkASmfmffFBdW8ac
Audit follow-up — the toggle behaviour now lives in a single
`ToggleableTimeAgoText` core in `ui/note/elements/TimeAgo.kt`. `TimeAgo`,
`NormalTimeAgo`, `ChatTimeAgo`, and `ChatroomHeaderCompose.TimeAgo` are
thin wrappers that pick a `TimeAgoStyle` (Dotted / Short) and pass
colour/font params; no per-site duplication of state + clickable +
derivedStateOf.
Performance fixes that matter for a feed with hundreds of timestamps:
- `rememberSaveable` → `remember`. Persisting a transient peek-toggle to
the SavedStateRegistry for every visible+scrolled-past note was pure
memory bloat. Recycling now resets to relative, which is the expected
behaviour for a transient inspect action.
- The relative-mode `derivedStateOf` lambda reads `nowState.value` only
when displaying a relative time. An item the user has frozen to its
absolute date no longer re-evaluates every 30-second tick.
- Core composable takes only stable primitive params (Long, Color,
TextUnit, TextOverflow, enum) so Compose can skip it entirely when
inputs don't change.
- Desktop wrapper dropped the redundant `derivedStateOf`: its formatter
reads no State, so derivedStateOf had nothing to observe.
https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
Every TimeAgo composable (Android NoteCompose timestamp, NormalTimeAgo,
ChatTimeAgo, ChatroomHeaderCompose last-message time) and every Desktop
timestamp Text (NoteCard, NotificationsScreen, ChatPane, ConversationListPane)
is now clickable and toggles to a scale-adjusted absolute date/time:
- same day → time only (e.g. "14:32"), locale-aware
- same year → "MMM dd, HH:mm"
- older → "MMM dd, yyyy"
State is hoisted per-call site via rememberSaveable so the toggle survives
scroll-induced disposal in lazy lists. Desktop call sites share a small
ToggleableTimeAgoText wrapper; Android keeps its existing composable shapes
and just gains a clickable modifier + state.
https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
Add an in-process local relay to Amethyst Desktop that persists all
received events to a per-account SQLite database using quartz's existing
EventStore infrastructure. On startup, the local store hydrates
DesktopLocalCache for instant feed rendering before remote relays connect.
- LocalRelayStore: manages per-account EventStore lifecycle, batched
write-through via BasicBundledInsert (250ms window), startup hydration
(contact list -> metadata -> recent content events)
- LocalRelayMaintenance: periodic cleanup (NIP-40 expiration, 30-day
prune, weekly VACUUM), disk space monitoring
- Settings UI: integrated into RelaySettingsScreen with statistics,
storage management (prune/vacuum/clear), JSONL export/import, and
error display
- OfflineBanner: animated banner in both SinglePaneLayout and
DeckColumnContainer showing offline status with local cache indicator
- Thread-safe store access via @Volatile + synchronized lock
- Skips re-enqueue during hydration (checks LOCAL_RELAY_URL)
- DB stored at ~/.amethyst/accounts/<pubkey8>/events.db
- Corrupt DB auto-detected and recreated on open
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a Namecoin record's value isn't valid JSON (a real failure mode
when an operator hand-builds the value and miscounts braces), the
NIP-05 path used to silently swallow the parser exception and surface
a misleading "no nostr field" message. That sends the publisher
chasing a phantom missing field when the actual problem is the value
itself.
Concrete case that triggered this: a `name_update` published a
474-byte d/testls value with one closing brace short of balanced. The
string parses up to the missing brace, after which kotlinx.serialization
throws "Unfinished JSON term at EOF at line 1, column 474". That error
was previously dropped, leaving the operator to debug "no nostr field"
without ever seeing the underlying JSON parse failure.
Changes:
- New NamecoinResolveOutcome.MalformedRecord(name, error). Distinct
from NoNostrField. The `error` field is the parser's own diagnostic
(e.g. "Unfinished JSON term at EOF at line 1, column 474") so the
publisher can locate the broken byte without spelunking.
- NamecoinNameResolver.performLookupDetailed: parse via a new
parseValueOrError helper and surface MalformedRecord instead of
collapsing into NoNostrField. Also rejects non-object top-level
values (arrays, primitives, null) with a useful diagnostic
("top-level value is JsonArray, expected JSON object").
- DesktopSearchScreen handles the new outcome by surfacing the parser
error verbatim in the Namecoin status banner, so the column number
reaches the publisher's screen.
Tests (commonTest / NamecoinImportTest):
- "NIP-05 lookup surfaces MalformedRecord with parser detail when
value is broken JSON": a deliberately one-brace-short value yields
MalformedRecord with a non-empty diagnostic.
- "NIP-05 lookup surfaces MalformedRecord when top-level value is a
JSON array": ensures non-object top-level values are rejected with
a useful "expected JSON object" message rather than silently
parsing as something unusable.
Tests don't pin the exact parser wording (kotlinx.serialization can
change it across versions); they only pin that the message is
attributed to JSON parsing rather than to a missing field.
The lazy-init declarations and CompositionLocalProvider entries were lost
during the rebase, so SearchScreen always saw null services and skipped
Namecoin resolution. Add them back inside the LoggedIn branch alongside
LocalTorState.
Re-add the Namecoin results UI block that was lost during the rebase.
Renders Loading/Resolved/NotFound/Error states above bech32/people/note
results when the query is a Namecoin identifier (.bit, d/, id/).
Adapt cherry-picked PR commits to current main where:
- ElectrumxServer.trustAllCerts was renamed to usePinnedTrustStore
- IRequestListener was renamed to SubscriptionListener
Move Namecoin service initialization from App-level (eager, on every startup)
to inside the LoggedIn branch (only created when user logs in and screens
that use Namecoin are reachable). Matches the Android lazy pattern in AppModules.
Also deduplicate NamecoinSettings: Android module now uses a typealias to the
commons module version, matching the original PR intent.
Eliminates code duplication between Android and Desktop:
- Move NamecoinSettings to commons/model/nip05DnsIdentifiers/namecoin/
(with @Serializable and @Stable annotations)
- Extract NamecoinResolveState sealed class to its own file in commons
- Move NamecoinSettingsTest to commons (shared by both platforms)
- Replace Android NamecoinSettings.kt with a typealias to commons
- Update all imports in Desktop and Android modules
- Remove debug println statements from ImportFollowListDialog and Main
Root cause: decodePublicKeyAsHexOrNull() returns truncated hex for
non-bech32 input (its else branch runs Hex.decode on arbitrary strings).
The relay then rejects the filter with 'Invalid author length'.
Fixes:
- Check raw 64-char hex FIRST (before bech32 parsing)
- Only attempt bech32 decode for strings starting with npub1/nprofile1/nsec1
- Validate all resolved pubkeys are exactly 64 hex chars
- Show specific error messages per identifier type instead of generic fallback
- Improved debug logging with pubkey prefix for each resolution path
Crash monitoring:
- Added Thread.setDefaultUncaughtExceptionHandler in Main.kt
- Logs crashes to ~/.amethyst-desktop-crash.log with timestamp and full stack trace
- Also prints to stderr for Gradle console visibility
The search bar was using resolve() which lets
NamecoinLookupException.ServersUnreachable propagate as an exception,
causing 'servers unreachable' to appear immediately instead of waiting
for the actual lookup to complete.
Switch to resolveDetailed() which catches exceptions internally and
returns typed outcomes (Success/NameNotFound/NoNostrField/
ServersUnreachable/InvalidIdentifier/Timeout). The search screen now
shows Loading for the full duration of the attempt (up to 20s) and
only shows the error after all servers have actually been tried.
The dialog was created but never accessible from the UI. Now:
- Added 'Import Follow List…' menu item in File menu (Shift+Cmd+I / Shift+Ctrl+I)
- showImportFollowListDialog state threaded through App composable
- Dialog rendered alongside ComposeNoteDialog and AddColumnDialog
Add censorship-resistant NIP-05 verification using the Namecoin blockchain
to the Desktop (JVM) app, porting functionality from PRs #1734, #1771,
New files:
- DesktopNamecoinNameService: app-level service wrapping the Quartz
NamecoinNameResolver with caching, custom server support, and live
state flows. Uses plain JVM sockets (no Tor support on Desktop yet).
- DesktopNamecoinPreferences: Java Preferences API-backed persistence
for Namecoin settings (enabled toggle + custom ElectrumX servers).
- NamecoinSettings: Desktop copy of the settings data class (no Android
dependencies).
- LocalNamecoin: CompositionLocals for threading Namecoin service/prefs
through the compose tree.
- NamecoinSettingsSection: Compose Desktop UI for configuring ElectrumX
servers — toggle, active server display with DEFAULT/CUSTOM badge,
add/remove custom servers, reset to defaults. Uses onPreviewKeyEvent
for Enter-to-submit instead of Android KeyboardActions.
- ImportFollowListDialog: Dialog for importing follow lists via npub,
hex, NIP-05, or Namecoin identifiers. Resolves identifiers to pubkeys
with Namecoin blockchain support.
Modified:
- Main.kt: Instantiate DesktopNamecoinPreferences and
DesktopNamecoinNameService in App composable, provide via
CompositionLocals, wire into RelaySettingsScreen.
- SearchScreen.kt: Detect Namecoin identifiers (.bit, d/, id/) in the
search bar, resolve via DesktopNamecoinNameService with loading/error
states, display resolved user above standard results. Uses
LaunchedEffect with key-based cancellation for stale lookups.
- DeckColumnContainer.kt: Pass namecoinPreferences to RelaySettingsScreen
from CompositionLocal.
Tests:
- DesktopNamecoinPreferencesTest: round-trip persistence, add/remove
servers, enable/disable, reset, duplicate handling.
- NamecoinSettingsTest: server string parsing/formatting, round-trips,
edge cases, toElectrumxServers conversion.
The Quartz KMP library (commonMain + jvmAndroid) already contains the
core Namecoin resolution code (ElectrumXClient, NamecoinNameResolver,
NamecoinLookupCache) shared across Android and Desktop.
The left navigation rail in single-pane mode renders a fixed list of
pinned screens (Home, Reads, Notifications, ...) plus a 'More' launcher
and a stack of bottom controls (relay health, bunker heartbeat, tor
status, account switcher).
Material3 `NavigationRail` lays its children out in a non-scrollable
`Column`. When the window is short — either because the OS window is
small or the user pinned several screens — the bottom items in the
list (and the 'More' button) get clipped and become unreachable.
Replace the `NavigationRail` with a `Column` that mirrors the rail's
container styling and splits content into two regions:
- A scrollable region (weight(1f) + verticalScroll) holding the pinned
screens and the 'More' launcher. Overflow now scrolls instead of
clipping.
- A fixed bottom region holding the relay health indicator, bunker
heartbeat, tor status indicator, and the account switcher. These
remain anchored at the bottom of the rail.
Item visuals are preserved by keeping `NavigationRailItem` for the
items themselves, with `NavigationRailItemDefaults.colors()`.
No behavior change when the rail content already fits the window.
Several user-visible error messages on Desktop are rendered with plain
`Text` composables, which means they can't be selected or copied. That
makes it awkward to share an error in a bug report or paste a hex error
string into a search.
Wrap the error text in `SelectionContainer` at the canonical sites:
- `commons.ui.components.LoadingState`: wrap the description in
`EmptyState` and the message in `ErrorState`. `EmptyState` is reused
as the in-feed error renderer (e.g. 'Error loading feed' with the
underlying error in `description`), so this covers feed/loading
errors across screens that use these helpers.
- `ComposeNoteDialog`: wrap the validation error and the upload error
in the compose-note dialog.
- `auth/LoginCard` (Nostr Connect): wrap the connection error.
- `auth/KeyInputField`: wrap the supporting-text error so the inline
message under the nsec input field can be copied.
No visual changes \u2014 `SelectionContainer` does not affect layout or
styling. Selection works on Compose Desktop (mouse drag) and on Android
(long-press) without further changes.
Replace manual relay subscribe + Channel approach with the proven
SearchBarState + rememberSubscription pattern (same as NewDmDialog).
This properly handles relay connection lifecycle and NIP-50 search,
returning results from all connected relays.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hoist author search state to FeedsDrawerTab (AlertDialog can't run LaunchedEffect)
- NIP-50 relay search via connected relays with Channel-based result streaming
- 28dp UserAvatar in author suggestion rows
- "No users found" empty state
- 32dp dialog margins, 300dp max results height, 30 result limit
- FindUsersTest: 8 tests verifying cache search with/without metadata
- Fix: use connectedRelays instead of unconnected DEFAULT_SEARCH_RELAYS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Linux build path of the vlc-setup plugin pulls vlc-plugins-linux from
Maven Central (ir.mahozad:vlc-plugins-linux), which only ships 3.0.20 and
3.0.20-2 — there is no 3.0.21 artifact yet. With vlcVersion set to 3.0.21,
both the CI pre-fetch step and the in-Gradle vlcDownload task hit a 404.
Match VLC_VERSION in build.yml and document the lag so future bumps wait
for the Maven artifact to catch up.
https://claude.ai/code/session_01GrZLMi3sdp6frwREmQ9cUi
- UserSearchEngine in commons (reusable, platform-agnostic) with RelayUserSearchDelegate interface
- DesktopRelayUserSearchDelegate using direct relay manager subscribe
- LocalDesktopCache + LocalRelayManager composition locals
- Provide cache/relay at App() level so AppDrawer and dialogs can access them
- FeedsDrawerTab reads LocalDesktopCache for author display names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- FeedDefinition data model with FeedSource sealed interface (Filter, Following, Global, DVM, PeopleList, InterestSet, SingleRelay)
- FeedDefinitionRepository with StateFlow, groupedFeeds, pin/unpin/reorder (max 3 pinned)
- JSON serialization via Jackson with round-trip tests (18 unit tests)
- SearchQuery.toFeedDefinition() bridge for creating feeds from search
- FeedBuilderDialog with author search (cache lookup + npub decode), hashtag/relay inputs, kind filter checkboxes, exclude authors/keywords, Cmd+S save, Enter to add
- FeedsDrawerTab in app drawer with edit/delete/pin/unpin actions
- Feeds tab in top header (FilterChips) with inline mode switching
- CustomFeedScreen + DesktopCustomFeedFilter + createCustomFeedSubscription for relay-based custom feed content
- FeedDefinitionEvent (kind 31890) in quartz for future publish/import
- Local persistence via java.util.prefs.Preferences (auto-save on change)
- DeckColumnType.CustomFeed for navigation integration
- Renamed Home to Feeds in nav rail
- Fix: AnimatedGifImage race condition (coerceIn on frame index)
- Fix: search results margins (sidePadding applied consistently)
- Fix: app drawer search includes feeds in results
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues fixed:
1. BunkerResponseDeserializer produces generic BunkerResponse for decrypt/encrypt
results (plain strings that aren't pubkeys or JSON). The response parsers only
matched typed subtypes (BunkerResponseDecrypt/Encrypt), causing all decrypt and
encrypt operations through NIP-46 bunker to fail with ReceivedButCouldNotPerform.
Fix: parsers now fall back to extracting response.result directly.
2. RemoteSignerManager timeout was 30s, shorter than Amber's 60s approval window.
Fix: increased to 65s with safe retry (republishes same request, preserving UUID
so bunker responses always match).
3. Desktop ChatPane showed raw ciphertext on decrypt failure instead of an error
message. Fix: shows "Could not decrypt the message" in italic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge upstream main into feat/desktop-multi-account, resolving:
- Main.kt: Surface wrapper + CompositionLocalProvider + nip11Fetcher from upstream, multi-account DeckSidebar/LoginScreen from HEAD
- FeedScreen.kt: viewport-aware scroll from HEAD + contentPadding from upstream
- DeckSidebar.kt: merged imports (multi-account + titleBarInsetTop + MaterialSymbols)
- Migrated AccountSwitcherDropdown + AddAccountDialog from material.icons to MaterialSymbols
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>