meltToLightning selects proofs covering amount+fee_reserve and, on
overshoot, swaps them down to exactly that before melting. But the melt
mints its inputs on the active keyset and the mint then charges its own
NUT-02 input fee on them — which the swap-down target didn't include. On a
fee-charging mint (mint.coinos.io active keyset = 100 ppk) the melt was
left a sat short and threw "Inputs total X < required Y", so fixing the
per-keyset swap fee alone just moved the failure from the swap to the melt.
Reserve activeKeysetInputFeeFor(required) on top of amount+fee_reserve for
both proof selection and the swap-down target so the subsequent melt has
room for its input fee.
Also fix the reported fee in MeltCompleted: the pre-paid swap "keep" was
split off before the melt and never spent, so subtract it instead of
counting the whole selected total minus change (which overstated fees by
the keep amount in the swap path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ffjz3doZ5CtFtAWSpvmqR
Two changes:
1. "Mine" top-nav filter for the Music and Playlists feeds. Adds a shared
`musicRoutes` option list (the content-style catalog plus "Mine") to
TopNavFilterState and points both music top bars at it. The local-cache
feed filters and the relay sub-assemblers now handle TopFilter.Mine by
restricting to the logged-in user's own tracks/playlists (by author, over
their outbox relays) — same pattern as the badges/communities feeds.
2. Fix: editing a track/playlist showed the empty upload placeholder even when
the event already had a cover. The shared CoverImagePicker now renders the
already-published cover URL (with tap-to-replace and a remove button) when no
new local file is picked. The track composer's clearPickedCover now also
clears the saved URL so "remove cover" sticks on save.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oofoSH7eMXrs2TCU4uncS
When melting/swapping Cashu proofs, the wallet computed the NUT-02 input
fee from the mint's currently-active keyset (`keyset.inputFeePpk`) for
every input. But NUT-02 charges the fee per the keyset each input proof
was minted under, which can be an inactive, rotated-out keyset with a
different fee.
On mint.coinos.io the old keyset (004f7adf2a04356c) charges 0 ppk and the
active keyset (007311aa2fa58cc8) charges 100 ppk. A wallet holding proofs
on the old keyset reserved a fee the mint never takes, leaving the swap
outputs one sat short — surfacing as "Mint Error (HTTP 400), inputs 84 -
fees 0 vs output (83) are not balanced" when sending over Lightning.
Fee is now `ceil(sum(input_fee_ppk_i) / 1000)` over each input's own
keyset, read from /v1/keysets (which lists inactive keysets too, unlike
/v1/keys). Applied in swap, swapToLocked, and meltProofs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ffjz3doZ5CtFtAWSpvmqR
Add in-playlist track management to the music playlist composer: each track in
the working list shows its artwork/title/artist with move-up, move-down and
remove controls. The list is seeded from the loaded event when editing and
published in its new order on save. Adding new tracks still happens via the
per-song "Add to playlist" sheet.
- quartz: MusicPlaylistEvent.edit() now takes the ordered track list and resets
the playlist's music-track `a` tags to it (preserving any non-track `a` tags,
the d tag, custom hashtags and other metadata). Add MusicPlaylistEventEditTest
covering reorder, removal, visibility switch, cover/description clearing and
tag preservation.
- amethyst: NewMusicPlaylistViewModel gains the working track list plus
moveTrackUp/moveTrackDown/removeTrackAt; NewMusicPlaylistScreen renders the
editable track section; new string resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oofoSH7eMXrs2TCU4uncS
Chatroom.addMessageSync evaluated `activeSenders + author` but discarded the
result, so `activeSenders` stayed permanently empty and
`Chatroom.senderIntersects()` always returned false. The Known-rooms filter is
`senderIntersects(follows) || hasSentMessagesTo(room)`, so with the follow path
dead a room only counted as Known once the user's own self-addressed NIP-17
gift wrap decrypted. Incoming DMs from followed contacts were misrouted to New
Requests, and the Known tab sat on the "Loading Feed" spinner (empty feed shows
the spinner until gift-wrap history exhausts — minutes with many relays/Tor).
Assign the new set. Prune/remove intentionally do not recompute activeSenders so
a room never flips Known->New when old messages are pruned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the name-only "new playlist" dialog with a full-screen create/edit
composer reachable via a dedicated route (Route.NewMusicPlaylist).
The composer surfaces a cover-image upload (same Blossom/NIP-96 pipeline as the
music-track composer), plus title, short description, long-form notes, a
public/private toggle and a collaborative toggle. An edit affordance now appears
on the user's own playlist cards.
- quartz: add MusicPlaylistEvent.edit() — updates the composer-owned metadata
while preserving the track `a` tags and every other tag of the prior version.
- amethyst: NewMusicPlaylistViewModel + NewMusicPlaylistScreen (create/edit/
delete); extract the shared cover-picker/placeholder/progress-banner into
MusicComposerUploadUi so the track composer reuses them; FAB now navigates to
the route; add edit icon on owned playlist cards; new string resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oofoSH7eMXrs2TCU4uncS
Attach the cached PNG's content URI as ClipData on the ACTION_SEND intent
so the Android share sheet itself gets read access and renders the image
preview at the top, instead of a generic file icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JM7z36GaqCm612rjPnX2G9
The Receive dialog polls the mint every 3s to see if the bolt11 has
been paid. Each poll flipped the flow state AwaitingPayment ->
Completing -> AwaitingPayment, and since the dialog renders a totally
different body for Completing (an "issuing proofs" spinner, no invoice,
no buttons), the invoice view was replaced by a spinner and then
recreated every 3 seconds — a constant flicker.
Add a `checking` flag to AwaitingPayment instead. The routine poll now
stays in AwaitingPayment and only toggles that flag, so the invoice (and
the Discard button) remain on screen and the dialog just swaps its
status line between "Waiting for the invoice to be paid…" and "Checking
the mint…". The full Completing body is shown only once payment is
actually confirmed and proofs are being issued.
The compareAndSet gate that prevents two concurrent polls from both
reaching completeMintFromLightning is preserved (now gating on
checking=false -> checking=true), with an extra early-out when a check
is already in flight.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjNqJZFxQinvuR4sGwpWQR
Adds a second "Share as Image" entry to a note's 3-dot menu that renders
the same framed card, captures it to a PNG and hands the local file
straight to the Android share sheet — no preview, no upload. The existing
upload-and-share-URL flow is renamed to "Share as Image Url".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JM7z36GaqCm612rjPnX2G9
Make BUILDING.md everything a fork needs to build and release, and add a
maintainer-facing ship checklist.
- BUILDING.md: full CI secrets inventory (Android keystore vs GPG/Maven, with
generation commands), a distribution-channels table (GitHub, Maven, Play,
F-Droid pull, Zapstore, Homebrew/Winget), and a "generated & vendored
artifacts" section (Material Symbols subset, Arti native libs).
- RELEASE_OPS.md (new, repo root): Amethyst-specific shipping — pre-tag
checklist, per-channel rollout (manual Play AAB, Zapstore `zsp publish` with
our nsec, F-Droid build-from-source), the push-notification server
(amethyst-push-notif-server), secret ownership, and verification.
- README.md: slim the Deploying section to point at both docs, fix the stale
quartz dependency version, and add a Maven Central badge plus JitPack
snapshot docs.
- android-expert skill: drop the duplicated build-config block, delegate to
gradle-expert, keep only the Android-specific flavor note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump the app version to 1.12.1 (versionCode 449) in the catalog, which now
drives every module. Add the v1.12.01 changelog (Health Connect workouts,
share-as-image, immersive system bars, deterministic Tor Active fix) and index
it. Refresh the quartz-integration skill's artifact-version references.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make every module read its release version from gradle/libs.versions.toml
instead of carrying its own literal, so a release bump is a single-file edit.
- Move the Android versionCode out of amethyst/build.gradle.kts into the
catalog as `appCode`; amethyst reads it via libs.versions.appCode.get().
- quartz publishes with version = libs.versions.app.get().
- geode generates a BuildConfig.VERSION from the catalog (new
generateVersionFile task) and RelayInfo.VERSION reads it, so the NIP-11
software version tracks releases.
- Update the root build comment to point at the appCode entry.
Version-neutral: everything still resolves to the current 1.12.0 / 448.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LocaleData.getMeasurementSystem requires API level 28 but the module's
minSdk is 26, which lint (NewApi) flagged as an error. Guard the ICU call
behind a Build.VERSION.SDK_INT check and fall back to a country-code based
heuristic on API 26-27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDcx7i1VDRyT7rDcuSB5Co
Replace the plain-text note with TranslatableRichTextViewer (inside
SensitivityWarning), the same renderer kind-1 notes use — so workout notes get
links, mentions, hashtags, embeds, image/URL previews and inline translations.
WorkoutDisplay now receives backgroundColor, canPreview, quotesLeft,
accountViewModel and nav; both call sites (NoteCompose, ThreadFeedView) pass
them through.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
- Hoist the constant systemBarsBehavior assignment out of the snapshotFlow collect loop
- Track the top in-app bar's collapse fraction (falling back to the bottom bar on screens with no top bar) instead of max(top, bottom)
Install a DisposableEffect in ZoomableImageDialog that hides both status
and navigation bars via WindowInsetsControllerCompat when the full-screen
media dialog opens, and restores them on dispose.
Add a Progress Log table to the plan summarizing what landed in this
worktree, with commit refs and a clear pointer to the next critical-path
work (Phase 1.4 App() smoke test, then Phase 2 relay seam, then Phase 3
benchmark harness, then Phase 4 baseline, then Phase 5.2 bootstrap-gate
fix). Phase 5.1 ships but its end-to-end delta is still pending the
benchmark harness.
Phase 5.1 of the launch-optimization plan: the cold-boot critical path
loaded /icon.png up to four separate times (taskbar setup, Window icon,
Tor splash, account-loading splash). Two of those sites also paid an
ImageIO.read to obtain a BufferedImage, and the Window-icon site
additionally round-tripped the image back through ImageIO.write so Skia
could re-decode it.
IconResources holds one lazy each for the bytes, the decoded
BufferedImage, the platform-adapted BufferedImage (squircle on macOS),
and the two BitmapPainters (raw + adapted). All four call sites in
Main.kt now consume the cached values directly — no remember, no
re-decode.
IconResourcesTest pins the memoization invariants (same instance on
repeated access). All 271 desktopApp tests pass.
End-to-end delta vs the baseline will be measured once Phase 3
benchmarks land; the worst-case savings on cold boot are two
ImageIO.read calls plus three resource reads plus one ImageIO.write,
all on the main thread.
Phase 1 of the desktop launch-optimization plan: pin the behavior of
the cold-boot critical path before any launch refactor lands.
* Plan document committed to desktopApp/plans/.
* AccountManager: ViewOnly load + decode-failure state transitions are
pinned by AccountManagerLoadStateTransitionsTest (2 tests).
* LocalRelayStore: gains a homeDir constructor parameter so tests can
point the SQLite event store at a temp directory; production callers
unchanged via default argument.
* LocalRelayStoreHydrationTest pins hydrate's contract:
- empty DB is a no-op,
- kind:3 contact list is consumed before kind:0 metadata,
- kind:1 within the 7-day window is hydrated,
- kind:1 older than 7 days is excluded.
All 266 desktopApp tests pass. No production behavior change.
- Render the kind 1301 content (the user's notes) below the stats grid in
WorkoutDisplay; previously it was published but never shown.
- Convert distance, pace, speed, elevation, and weight to the viewer's preferred
measurement system (phonePrefersMiles) via WorkoutInfo.inUnits, so a workout
stored in km shows in miles for an imperial viewer (and vice versa).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
Read the phone's measurement preference (Android 14+ Regional preferences
override via the locale's -u-ms- extension, else ICU's locale-derived
US/UK = miles) and default the New Workout composer's km/mi selector to it.
Health Connect distances (always metres) are converted into the chosen unit on
pre-fill instead of being forced to km.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
Instead of a generic "health_connect" source, capture the app/device that wrote
the record (the Health Connect data origin) and use it as the workout source —
so the card shows e.g. "SAMSUNG HEALTH" / "GOOGLE FIT" instead of "HEALTH
CONNECT".
resolveSourceName resolves the data-origin package to the installed app's
label, falling back to a known-package map (Samsung Health, Google Fit, Fitbit,
Garmin, Strava, Nike Run Club), then the raw package, then "Health Connect".
DetectedWorkout carries this as `source` and it flows into the published kind
1301 source tag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
Per request, the Workouts feed no longer has any Health Connect code. Both the
permission prompt and the detected-workout list now live in the New Workout
carousel: it shows a Connect card when permission is missing and the workout
list once granted.
- Remove WorkoutConnectBanner and the feed wiring; revert the FeedLoaded header
slot that fed it.
- Restore the Connect prompt in DetectedWorkoutCarousel (tri-state permission:
unknown renders nothing, denied shows Connect, granted shows the list).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
Refine the previous change: the Workouts feed shows a connect-only Health
Connect banner (a new WorkoutConnectBanner) so permission can be granted there,
but never lists detected workouts. The detected-workout list is shown solely in
the New Workout composer's carousel, which reverts to list-only (no connect
card).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
Per request, Health Connect suggestions no longer appear on the Workouts feed.
The integration now lives solely in the New Workout composer's carousel.
- Remove the feed banner wiring from WorkoutsScreen and revert the FeedLoaded
header slot that fed it.
- Delete the banner (WorkoutSuggestionCard), its state holder
(WorkoutSuggestionState), and the handled-id store (HealthConnectStore);
the carousel shows the full window and needs no dedup. LOOKBACK_DAYS moves to
HealthConnectManager.
- Fold the Connect prompt into the carousel so Health Connect permission can
still be granted (the banner was the previous entry point) — it shows a
Connect card when permission is missing, the carousel once granted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P