With WhileSubscribed, nothing held a strong reference to my own 31989
notes while no UI was collecting; under memory pressure the soft cache
could drop them, and the next recommendApp would rebuild that kind's
event from an empty snapshot, wiping previously recommended apps.
Eagerly keeps the observer registered (its result set strongly
references the notes), matching the sibling account states.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
Follows the Account state-class convention (InterestSetsState et al.):
the observed flow, the publish mutex, and recommendApp/unrecommendApp
now live in model/nip89AppHandlers/AppRecommendationsState, publishing
through account.sendMyPublicAndPrivateOutbox. Account just links it as
val appRecommendations = AppRecommendationsState(signer, cache, scope).
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
Account.myAppRecommendations is now LocalCache.observeEvents (indexed
kind+author filter) stated in the account scope, seeded with a
synchronous scan so the editor's first frame still sorts correctly.
The editor and the Recommend button collect it instead of rescanning
the cache on every newEventBundles emission; isAppRecommended is gone.
The synchronous scan stays only as the seed and for the mutex-guarded
read-modify-write publishers, which must read current cache truth.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
Offers can be configured (via nmanage / ShockWallet) to require payer
fields; Lightning.Pub rejects requests missing them with the misleading
"Invalid Offer" (code 1) reply. A flag to attach payer_data makes such
offers testable from the CLI.
https://claude.ai/code/session_01Fh7MRv8477pJiAJZ7yF87r
Clicking an app in the recommendations editor (or any 31990 link)
navigated to Route.Note(event.id) — the per-id version note, which the
soft cache evicts and relays can't serve for replaceables — leaving the
thread stuck on 'Event is loading or can't be found'. Route by
addressTag() like the generic AddressableEvent branch already does.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
Searching for an app name (e.g. "Amethyst") was returning every event
published through that client, because the local-cache note search
matched the search term against all tag values including the NIP-89
["client", ...] tag. Skip the client tag when matching tag values in
findNotesStartingWith.
https://claude.ai/code/session_01YMs6aXuvs5NaYjzyPH6Zqj
- New ByAuthorChip ('by <picture> <name>' pill, tappable to the profile,
with the following-checkmark overlay on the picture) shown under the
app name in the recommendations editor rows and on the app definition
card, so apps from known authors are distinguishable from impersonators.
- The editor's ordering pins now initialize from cached data instead of
empty sets, so the first frame after returning from an item sorts the
same as before leaving; previously the restored scroll anchor jumped
down the list when the recommended/follows tiers kicked in.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
- Account.kt: collapse three identical backend-not-configured Failure
constructions into one helper
- OnchainZapSendError: declare causeIsUserFacing on the enum so the
sender owns which failures carry a human-readable cause, instead of
the UI mapper hardcoding the list
- SendPaymentScreen: fee chip label is now a single format resource
instead of manual string concatenation
Source-identical values (Cashu, Lightning, On-chain, and per-locale
loanwords like Meditation/Yoga/Workouts in de) are intentionally left
to the English fallback since Crowdin strips them on export anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Surfaces relays unresponsive for 7+ days across the user's NIP-65 (10002),
DM (10050), and Search (10007) relay lists. A non-modal banner appears
above feed columns (and above the single-pane content) whenever the
classifier finds anything; tapping it opens an anchored Popup with one
row per unhealthy relay and per-row Remove / Open Dashboard / Snooze 7d
actions plus a banner-level "Snooze all 7d".
Quartz
- RelayStat gains best-effort lastConnectAt + lastIncomingAt timestamps
(epoch seconds, 0 = never observed). RelayStats listener pushes them
on onConnected / onIncomingMessage. Durable per-relay history lives
outside quartz in the commons RelayHealthStore.
Commons (new commons/relays/health/ package)
- classifyRelayHealth() pure function with the v1 gates:
* first-run grace (don't flag for 7d after firstScanAt)
* offline grace (don't flag if no relay anywhere has responded)
* Tor-mode skip (relay timing is intentionally lossy through Tor)
* per-relay snooze (snoozedUntil > now)
* 10006 (blocked) excluded from detection but still part of the
multi-list Remove action
- RelayHealthStore (account-scoped, supervised scope, 5s debounced
persist, 60s ticker for snooze expiry).
- RelayHealthListener wires the quartz lifecycle into the store.
- RelayHealthPersistence interface (no expect/actual — single impl per
platform via injection).
- RelayListMutator interface + RelayRemovalResult sealed type.
- Shared UnhealthyRelayBanner (errorContainer @ 50% alpha) and
UnhealthyRelayRow (static outlined tag chips, no ripple) composables.
- 8 classifier unit tests covering each gate + multi-list membership.
Desktop wiring
- PreferencesRelayHealthPersistence (java.util.prefs.Preferences, per
account via 8-char pubkey prefix).
- DesktopRelayListMutator runs the 4 sign-and-broadcast jobs in
parallel via async/awaitAll so a slow NIP-46 bunker doesn't multiply
latency by 4.
- Banner placed in DeckColumnContainer + SinglePaneLayout, store +
listener + per-account scan trigger wired in Main.kt's MainContent.
Scope: Desktop only for v1. Android wiring is intentionally not in
this PR — the commons module is platform-neutral and ready for Android
to follow whenever someone wants to pick it up.
- App card: '+n' kinds overflow is now a chip with the same metrics as
KindChip so it centers with the others; Recommend button is compact
(32dp), sits flush with the banner's right edge, and gains breathing
room below the cover.
- Recommendations editor: hides unnamed kind 31990 events (kept only if
already recommended) and only opens an EventFinder subscription for
rows whose definition isn't in cache yet, so the list stops filling
with 'Unnamed app' rows that never resolve.
- Editor ordering: the follow list is now collected as it loads (it was
snapshotted once at screen entry, so the follows tier was empty when
the screen opened before kind 3 loaded) and freezes on first edit like
the recommended tier.
- NoteCompose/ThreadFeedView: render kind 31989 as a 'Recommended apps'
card with the target kind chip and tappable app chips.
- Search: collapse the per-kind 31989 fan-out to the newest event per
author so one user's recommendations don't flood the results.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
Redesign the three payment cards rendered in the middle of a post
(Lightning invoice, CLINK Offer, Cashu token) around a shared PaymentCard
scaffold that follows the wallet screens' Material3 idiom: tonal card,
icon + label header with a copy action, centered headline amount, and a
full-width themed Pay/Redeem button (no more hardcoded white text or
7sp mint lines).
Descriptions were not being rendered at all:
- BOLT-11: LnInvoiceUtil only decoded the amount from the HRP. Add
tagged-field parsing (description 'd', expiry 'x', timestamp) with
BOLT-11 spec-vector tests; the invoice card now shows the memo and
flags expired invoices (Pay disabled). Desktop card shows it too.
- Cashu: V3 'memo'/'unit' and V4 'd'/'u' were parsed then dropped.
CashuToken now carries them; the card shows the memo and no longer
mislabels non-sat units (usd/eur cents formatted as decimals).
- CLINK Offers: the card now shows who gets paid (avatar + name from
the pointer's pubkey, tappable to the profile).
https://claude.ai/code/session_019VuZ4y3ij6Ly4VVExE1W1W
Sort the management list as: my recommended apps, then apps authored
by my follows, then the rest (most recent first within each tier).
The ordering snapshot freezes at the first toggle so rows don't jump
mid-edit; deselected apps stay in place until the screen is reopened.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
- Profile 'Apps' section now mirrors the Badges component: header with
count and a Settings icon (own profile only) that opens a new
management screen at Route.ProfileAppRecommendations.
- New ProfileAppRecommendationsScreen lists known kind 31990 app
definitions (recommended first) with toggles that publish/remove the
per-kind 31989 recommendation events, backed by a new relay
subscription for the user's 31989s and recent 31990 candidates.
- Account gains recommendApp/unrecommendApp with mutex-serialized
read-modify-write per d-tag, mirroring the profile-badges flow.
- Profile recommendations render as logo+name pills instead of bare
35dp icons; in-post app definition cards now show platform
availability (web/android/ios), handled event kinds as chips, and a
Recommend/Recommended button.
- Quartz: AppDefinitionEvent.platformLinks() reader and
AppRecommendationEvent.buildFromTags() to rebuild a 31989 while
preserving other apps' tags; round-trip tests included.
https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
Pay from, zap receipt, amount preset, and on-chain fee chips wrap with
the same 8dp row gap as the Receive-on pills. Material chips reserve a
48dp interactive height around their 32dp visual, inflating wrapped-row
gaps to ~24dp; the new shared ChipFlowRow drops that enforcement inside
the chip groups so the spacing matches.
- All chip FlowRows on the Send Payment screen (Receive on, Pay from,
zap types, amount presets, fee tiers) now declare an 8dp vertical
arrangement so wrapped lines get the same gap as the in-row spacing —
the custom Receive-on pills had no intrinsic padding and touched when
the row broke.
- The cashu rail chip and the cashu 'Pay from' wallet chip use the Cashu
vector mark (tinted with the chip content color) instead of the
generic wallet symbol.
The detail text under the Send Payment rail selector (e.g. the lightning
address, already visible in the recipient header) is gone. Instead every
chip carries its destination via long-press copy:
- Send Payment 'Receive on' chips copy the lightning address, the
noffer pointer, the on-chain destination (announced target address or
the pubkey-derived Taproot address — otherwise invisible), and the
shared cashu mint URL. The chips are now custom selectable pills since
M3 FilterChip has no long-press support.
- Profile rail chips gain the same copy values (clink noffer, derived
taproot address, mint URL) alongside the existing lightning and
payment-target copies.
- The announced bitcoin address moved from the detail line into the
on-chain receipt note so it stays visible when it differs from the
derived address.
The wallet-rail chips and the NIP-A3 payment-target chips were two
separately padded FlowRows, so the gap between the two rows was about
double the in-row spacing. The target chips now render inside the same
FlowRow as the rail chips, wrapping together with uniform 6dp spacing.
DisplayPaymentTargets is gone; PaymentTargetChip is rendered by
DisplayPaymentRailChips directly.
- Replace the NewWorkoutDialog with a Route.NewWorkout full screen
(NewGoalScreen pattern: NewWorkoutViewModel + PostingTopBar), which
also fixes the audit-found race where the dialog closed before the
signer finished — the screen now pops back only after a successful
sign/broadcast, keeping external-signer (Amber) flows alive.
- Move template building from Account.sendWorkout into the ViewModel.
- Render workout cards in thread view (ThreadFeedView fall-through gap).
- Snapshot parsed workout tags once per note (remember + WorkoutInfo)
instead of re-scanning the tag array on every recomposition.
- Fix Double.trimmed() Int overflow on absurd distances.
- Flatten the distance-unit chip layout.
https://claude.ai/code/session_01Kpx53UEeJqqR7CASzMu6GB
The Cashu vector is a monochrome black outline meant to be tinted like a
Material Symbol — with Color.Unspecified it rendered black and vanished
on dark backgrounds. Tint it with the chip color and lift the purple
from 0xFF7E57C2 to 0xFFA855F7 so the chip reads on both themes.
Replaces the lightning-address icon+text row with a chip and completes
the rail set, so every way to pay a profile reads as one chip row:
- Lightning chip shows the lud16 (long-press still copies it) and opens
the Send Payment screen on the Lightning rail.
- CLINK Offer chip moves into the same row (same look as before).
- New On-chain chip (when the chain backend is configured) and Cashu
chip (when the logged-in user's cashu wallet shares a mint the
recipient accepts), each opening their rail on the Send Payment
screen.
- All four render through a shared ProfilePaymentChip pill that matches
the NIP-A3 payment-target chips, replacing the old DisplayLNAddress
row and the one-off clink chip in DrawAdditionalInfo.
https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa