- FeedTabsHeader wrapped in Surface card (border + rounded corners)
- Feed tabs compact with spacedBy(4dp), removed "+ More" (in sidebar)
- SearchPill centered with weight(1f), expands inline to BasicTextField
with DropdownMenu showing recent + saved searches
- Removed Dialog-based SearchSpotlight (wrong UX — created separate AWT window)
- Cmd+F now navigates to Search column via navigateToScreen callback
- All feed tab types (Following/Global/Custom) properly handled in onClick
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SearchSpotlight: global overlay with scrim, auto-focused input, recent/saved
searches, hashtag quick-jump, and "Open full search" link
- SearchPill: compact reusable pill component with hover highlight and shortcut hint
- Cmd+F keyboard shortcut wired in MenuBar to open/close spotlight
- SearchPill added to FeedTabsHeader (Home/Feeds column headers)
- Result selection dispatches to deckState (deck mode) or singlePaneState (single-pane)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename DeckSidebar -> MainSidebar (it's not deck-specific anymore)
- Hoist MainSidebar from DECK-only to shared Row in Main.kt
- Remove inline 80dp NavigationRail from SinglePaneLayout
- SinglePaneLayout now only renders content pane (sidebar is external)
- onNavigate dispatches to singlePaneState.navigate() or deckState.addColumn()
based on active layoutMode
- Active item highlighting works in both modes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hardcoded date/time patterns ignored the user's Locale (date order:
dd/mm/yyyy vs mm/dd/yyyy vs yyyy-mm-dd) and the system 12/24-hour
override. Replace them with locale-aware formatters that resolve order
from the active Locale via DateFormat.getBestDateTimePattern() and pick
the time-of-day pattern via DateFormat.is24HourFormat(context).
- TimeAgoFormatter (amethyst + commons): build SimpleDateFormat from
Unicode LDML skeletons (yMMMd / MMMd / yMMM) so "May 28, 2026" in
en-US becomes "28 May 2026" in en-GB, "28.05.2026" in de-DE, etc.
- CalendarTimeFormat: same skeleton approach for date pieces; time
uses DateFormat.getTimeFormat(context) so a 24-hour Android user
sees 14:32 even on a 12-hour locale.
- New LocalizedDateTimeFormat helper with formatMonthDayTime,
formatMediumDate, formatMediumDateTime — used by wallet, vanish,
attestation, namecoin, eventsync screens to replace inline
SimpleDateFormat("MMM d, HH:mm") / ("MMM dd, yyyy hh:mm a") etc.
- Material3 TimePicker callers (calendar/nest/poll/zap-poll/expiration
date pickers + vanish request) now pass is24Hour from the system
setting instead of hardcoding false.
- Desktop article/reads/highlights screens use
java.text.DateFormat.getDateInstance(MEDIUM, locale).
- Drop dead formattedDateTime() in RelayCompose (was unused).
Intentionally left alone: notification feed bucket keys ("yyyy-MM-dd"
used as Map keys), TakePicture file naming (Locale.US), iCalendar
RFC 5545 stamps, NIP-52 ISO date storage, internal logging, and
ThreadLevelCalculator sort keys — none are user-facing.
Brings Amethyst Desktop to feature parity with Android for the Namecoin
resolution backend stack landed in #3056 / #3068. Three pieces:
- DesktopNamecoinPreferences now persists backend, namecoinCoreRpc,
fallbackToCustomElectrumx and fallbackToDefaultElectrumx (KEY_BACKEND
/ KEY_CORE_RPC / KEY_FALLBACK_*), mirroring NamecoinSharedPreferences.
Mutators are non-suspend because java.util.prefs is synchronous,
unlike Android's coroutine-backed DataStore. The Jackson mapper now
rejects unknown properties on read so kotlinx `@Serializable`
computed getters (e.g. NamecoinCoreRpcConfig.isUsable) round-trip
cleanly through java.util.prefs.
- DesktopNamecoinNameService takes an optional OkHttpClient provider,
lazily constructs a NamecoinCoreRpcClient when supplied, and builds
a fresh CompositeNamecoinBackend per lookup based on current
NamecoinSettings. Same shape as AppModules#buildNamecoinBackend.
Exposes the underlying RPC client (rpcClient) and a probeCoreRpc(cfg)
helper for the Settings Test RPC button.
- NamecoinSettingsSection gains a backend radio selector, a Core RPC
subform (URL / username / password / Save / Test RPC) with a TOFU
cert-pin AlertDialog mirroring Android's NamecoinCoreRpcSection, and
a fallback toggles section. The same KEY_PINNED_CERTS list is shared
with both ElectrumXClient and NamecoinCoreRpcClient via
setDynamicCerts(...), matching Android's behaviour where both
backends consume one trust store.
- Main.kt wires DesktopHttpClient.currentClient() in as the Core RPC
HTTP provider so .onion RPC URLs flow through the existing Tor
routing without extra plumbing, and propagates the new mutators to
the Settings UI.
- Extends DesktopNamecoinPreferencesTest with 8 new cases covering
default state, backend round-trip, Core RPC URL/user/pass/pin-flag
round-trip, fallback toggles, reset clearing, and a full
multi-field round-trip across a fresh preferences instance.
Verification on the canonical workspace clone:
- ./gradlew :commons:jvmTest --tests *Namecoin* — BUILD SUCCESSFUL
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:compileKotlin :desktopApp:test — BUILD SUCCESSFUL
- ./gradlew :amethyst:testPlayDebugUnitTest --tests *Namecoin* — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck
:desktopApp:spotlessCheck — BUILD SUCCESSFUL
Mirrors Android's NamecoinSharedPreferences pinned-cert API on Desktop so
user-accepted TLS pins survive process restart. Same JSON-list shape, same
distinct-append semantics, same wipe-on-reset behaviour.
What's new
- DesktopNamecoinPreferences gains addPinnedCert / loadPinnedCerts /
clearPinnedCerts (sync rather than suspend, since java.util.prefs is
synchronous). reset() now clears pinned certs too, matching Android.
- DesktopNamecoinNameService accepts a pinnedCertsProvider and pushes the
loaded list into ElectrumXClient.setDynamicCerts at init, mirroring
Android's AppModules.kt wiring. Exposes the underlying client so the
Settings UI can call testServer() and re-apply pins live.
- Desktop NamecoinSettingsSection grows an optional Test Connection + TOFU
pin sub-section: runs ElectrumXClient.testServer per active server,
collects PEM + SHA-256 fingerprint from successful TLS handshakes, and
prompts the user to pin each new cert via AlertDialog. UI hidden when
no service is wired (so existing call sites stay valid).
- Main.kt wires both halves together and updates the freshly-pinned cert
list into the live client without waiting for restart.
Persistence is plain java.util.prefs (same backing store as the rest of
DesktopNamecoinPreferences) — explicitly NOT EncryptedSharedPreferences.
Pinned cert PEMs are public material; no secrets stored.
Tests
- DesktopNamecoinPreferencesTest: +6 cases covering empty default,
persistence + reload, dedup, blank input ignored, reset wipes, and
independence from settings copies.
Verification
- ./gradlew :desktopApp:compileKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:test — BUILD SUCCESSFUL (16 tests, 0 failures)
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck :desktopApp:spotlessCheck — BUILD SUCCESSFUL
Stack note
Stacked behind #3072 (merged 2026-05-27). Next: PR-C for the full
Namecoin Core RPC backend + composite fallback persistence + UI.
Pins each per-user replaceable note to the User's lifetime so weak-ref
eviction from LocalCache.addressables can't lose them — same fix the
NIP-65 / DM relay list notes already had, generalised so adding new
pinned kinds is a one-liner.
Background: LocalCache.addressables is a LargeSoftCache<Address,
AddressableNote> backed by WeakReference. Without a strong reference
somewhere, an addressable note shell (and any event loaded into it) can
be cleared on any GC cycle even though it was successfully delivered.
The User constructor already held nip65RelayListNote / dmRelayListNote
fields exactly to defeat this for kinds 10002 and 10050. kind:10019
(NutzapInfoEvent) had no such pin, so the zap picker's "does this user
accept nutzaps?" check would silently return null for an evicted note —
the chip never showed even when the recipient had actually published.
This refactor:
1. Adds `UserContext` — a one-method `fun interface` exposing
`addressableNote(addr): Note`. User holds it for life; LocalCache
implements it via a single instance bound to ::getOrCreateAddressableNoteInternal.
2. Converts the three per-user pinned notes (nip65 / dm / nutzapInfo)
to `by lazy` fields backed by the context. Each is resolved the
first time it's read and then held by the User's strong reference
until the User itself is collected. `by lazy`'s default SYNCHRONIZED
mode handles concurrent reads from the zap picker + wallet state.
3. Adds typed accessors on User: nutzapInfo(), acceptsNutzaps(),
nutzapMints(), nutzapP2pkPubkey() — mirrors the existing
authorRelayList() / dmInboxRelayList() shape.
4. CashuWalletState.peekNutzapTarget now reads via
`cache.getOrCreateUser(recipientPubKey).nutzapInfo()` instead of
touching the cache's addressable map directly.
Tradeoffs vs the eager-constructor approach:
- No upfront allocation for kinds the screen never reads.
- Adding a new pinned kind (mute list, blocked relays, bookmark list)
is one `by lazy { context.addressableNote(...) }` line in User —
no constructor-signature churn across call sites.
- User now depends on a narrow `UserContext` interface; test fakes are
a one-liner: `User(hex) { addr -> Note(addr.toValue()) }`.
Migration:
- Single User constructor call site (LocalCache.getOrCreateUser) updated.
- Two existing test fakes (NoteOnchainZapTest, SearchResultSorterTest)
switched to the SAM-lambda form.
- No external behaviour change — the public `nip65RelayListNote` /
`dmRelayListNote` fields keep the same names and types, so the few
consumers (RelayFeedViewModel, ChatNewMessageViewModel) need no edits.
https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
- Wire DragAndDropTarget on avatar circle and banner area
- Image-only filter (jpg/png/gif/webp/avif)
- Visual drag-over feedback (primary border highlight)
- Fix avatar: only show placeholder icon when no image set
(previously overlay was visible behind the loaded avatar)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the awkward small icon button with a full 100dp tappable circle.
Shows surfaceVariant background when empty, semi-transparent overlay with
centered upload icon when image is present. Spinner replaces icon during
upload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audit pass after LocalCache wiring: MusicTrackEvent (36787) and
MusicPlaylistEvent (34139) were materialized and rendered, but a dozen
filter sites still listed AudioTrackEvent.KIND alone, so music events
silently dropped out of feeds, search, notifications, and relay labels.
Mirrors the AudioTrackEvent pattern in every place AudioTrackEvent.KIND
or `is AudioTrackEvent` appears:
Feed display filters (acceptableEvent + ADDRESSABLE_KINDS):
- HomeNewThreadFeedFilter
- FollowPackFeedNewThreadFeedFilter
- UserProfileNewThreadFeedFilter
- UserProfileMutualFeedFilter
- HashtagFeedFilter
- GeoHashFeedFilter
Relay subscription kind lists:
- FilterPostsByGeohash (PostsByGeohashKinds)
- FilterPostsByHashtags (PostsByHashtagKinds2)
- FilterPostsByRelay (PostsByRelayKinds2)
- SearchPostsByText (SearchPostsByTextKinds1)
- Desktop SearchFilterFactory.defaultKindGroup1
Notifications:
- NotificationFeedFilter.ADDRESSABLE_KINDS (NOTIFICATION_KINDS picks
this up automatically)
Relay information screen:
- kindDisplayName → R.string.kind_music_track / kind_music_playlist
(with the two new string resources)
Replace the single-field display name AlertDialog with a comprehensive
profile editing Dialog supporting all 13 Nostr profile fields: name,
display name, about, avatar, banner, website, pronouns, NIP-05,
lightning address, LNURL, and NIP-39 social proofs (Twitter, GitHub,
Mastodon).
New shared EditProfileFields state holder in commons/commonMain using
MutableStateFlow (matching ChatNewMessageState pattern) benefits both
Android and Desktop platforms.
Desktop-native features:
- Blossom image upload via DesktopFilePicker + UploadOrchestrator
- Live NIP-05 verification with debounced network check
- Keyboard shortcuts: Ctrl+S/Cmd+S save, Esc cancel
- Unsaved changes confirmation dialog
- Collapsible social proofs section
- Avatar/banner URL live preview via AsyncImage
- ProfileBroadcastBanner for relay broadcast feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pure text paragraphs now render as a single Text composable instead
of individual words in FlowRow, eliminating unwanted inter-word gaps.
Co-Authored-By: Claude <noreply@anthropic.com>
Type @ followed by a name in the compose/reply dialog to see a
dropdown of matching users from the local cache. Selecting a user
inserts their nostr:npub reference. Shows avatar + display name +
truncated npub.
Co-Authored-By: Claude <noreply@anthropic.com>
Test referenced formatSats, DEFAULT_ZAP_AMOUNTS, and ZapType which
were removed/made private in upstream merge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite SendDialog with sealed state machine that auto-detects input
type (BOLT11, LNURL bech32, lightning address). For LNURL/address:
resolves endpoint, shows amount form with min/max hint, optional
comment field, fetches invoice, then pays via NWC. Strips lightning:
URI prefix. Inline copiable errors with retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SendDialog: switch to Dialog+Card with X close, inline copiable error
messages, button resets to "Pay Invoice" on error for retry.
LightningAddressResolver: return error body from callback responses so
server error messages (e.g. "Recipient wallet error") surface to user
instead of generic "Failed to fetch invoice". Also check "message"
field in addition to "reason" for error extraction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace AlertDialog with Dialog+Card pattern. Invoice created state now
shows centered amount, description, 240dp QR code, and full-width
"Copy Invoice" button. Close via top-right X button. Input form gets
full-width "Create Invoice" button.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LaunchedEffect that rescans cache when followedUsers populates after
startup, fixing empty feed when contact list arrives after initial scan.
Remove diagnostic println from NwcPaymentHandler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LoginScreen's fire-and-forget save coroutine used rememberCoroutineScope
which got cancelled when the composable left composition after login.
Move saveCurrentAccount() to onLoginSuccess in Main.kt which uses the
app-level scope that survives recomposition. Fixes both nsec login and
generate-new-account flows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add kind 1 (replies) to interaction subscriptions
- Key count reads on FlowSet state for reactive updates
- Wire Quote menu item to ComposeNoteDialog with q-tag support
- Add BoostsPopup on long-press repost icon (who boosted)
- ComposeNoteDialog now accepts quoteOf param with nostr: URI pre-fill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove premature ensureRelayConnected check — NostrClient connects
on subscribe/publish via sendOrConnectAndSync
- Fix disconnect crash: use appScope instead of rememberCoroutineScope
to survive recomposition when nwcConnection goes null
- Surface balance errors/timeouts as snackbars instead of silent swallow
- Add ensureRelayConnected helper to RelayConnectionManager
- Add Phase 2 embedded wallet research doc
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit dropped `authors` and `#p` from the relay subscription
filter to match Primal's interop shape. Without those, the relay will
deliver any signed kind-23195 event that carries our request id in `#e`,
so an attacker who can observe the request on the relay could forge a
"response" with their own keypair: Amethyst would happily derive a shared
secret from `event.pubKey` (the attacker), decrypt the payload, and
display attacker-controlled balance/transaction data. Even worse,
`paymentTracker.onResponseReceived` removed the pending entry on first
match — so the legitimate wallet reply that followed was silently dropped.
Move the author check from the relay layer into NwcPaymentTracker:
- `registerRequest` now requires the expected wallet-service pubkey
(read from the request's `p` tag). LocalCache extracts it during
`consume(LnZapPaymentRequestEvent)` and refuses to register if the
request has no `p` tag.
- `onResponseReceived` takes the response author and returns a sealed
MatchResult of NoMatch / WrongAuthor / Matched. A WrongAuthor result
leaves the pending entry in the map so the legitimate response can
still resolve it.
- Android LocalCache and DesktopLocalCache both adopt the new API and
log a warning on suspected spoof attempts.
End-to-end the response is still encrypted under the per-connection shared
secret, so this is a second layer of defence rather than the only one,
but matching the author keeps a forged kind-23195 from consuming the
pending slot and DoSing the legitimate reply.
clearNwcConnection and setNwcConnection now require npub param and are
suspend functions. ConnectWalletDialog validates URI prefix inline
before firing the async connect callback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Map Desktop ZapType (Public/Private/Anonymous) through to
LnZapEvent.ZapType and pass to ZapAction.fetchZapInvoice
- Add relayHint + authorRelayHint params to NoteActionsRow;
FeedScreen now passes Note.relayHintUrl() for reactions/reposts
- Replace metadata preload TODO with design rationale comment
- Add 70-test manual testing sheet for wallet & zapping features
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Send, Receive, and Connect are now AlertDialogs instead of full-screen
sub-pages. The wallet home content is centered with widthIn(max=360.dp)
for a polished desktop look. Dialogs are the native desktop pattern
(consistent with zap dialog, bookmark dialog, tor settings dialog).
- ConnectWalletDialog: NWC URI input with paste + validation
- SendDialog: BOLT11 invoice input with paste + progress
- ReceiveDialog: amount/description input, transitions to show
generated invoice with copy button
- Home content: centered balance card + action buttons + connection info
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getBalance and makeInvoice were publishing the request event before
subscribing for the response. Fast wallet responses (like get_balance)
would arrive before the subscription was active, causing timeouts.
Fix: subscribe first via onSubscribed callback in waitForGenericResponse,
then publish. The original payInvoice wasn't affected because Lightning
routing takes long enough for the subscription to be ready.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wire AccountManager.setNwcConnection() and clearNwcConnection() into
wallet column for persistent connect/disconnect
- Implement NwcPaymentHandler.getBalance() via NIP-47 get_balance RPC
- Implement NwcPaymentHandler.makeInvoice() via NIP-47 make_invoice RPC
- Add generic waitForGenericResponse() helper for NWC RPC operations
- Auto-fetch balance on wallet column load via LaunchedEffect
- Wire receive screen to generate real invoices via NWC
- All wallet column features now functional (no blocking TODOs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>