When the initial chatroom list picked a ChannelMetadataEvent or
ChannelCreateEvent as the representative note for a public channel
(because no ChannelMessageEvent had been seen yet), the arrival of a
later ChannelMessageEvent caused updateListWith to append a second
entry for the same channel — its match check only recognized old
notes whose event was a ChannelMessageEvent. Two notes for the same
channelId then produced the same PublicChannelLazyKey in the LazyColumn
and crashed with IllegalArgumentException: Key was already used.
Extract the channel id from any of the three public-chat event types
when matching the existing entry so the new message replaces the
placeholder instead of duplicating it.
PaymentButton and DisplayPaymentTargets read note?.event inside a
remember(note) keyed on the AddressableNote reference, which is stable
across event arrival. They also rely solely on LoadAddressableNote's
cache lookup with no relay subscription, so when viewing a profile
outside the path that activates UserProfileMetadataFilterSubAssembler
(or before the event arrives) the UI never updates.
Match the canonical pattern from ProfileBadgesScreen: pair
LoadAddressableNote with EventFinderFilterAssemblerSubscription to
request the event from relays, and observeNoteEvent<PaymentTargetsEvent>
to recompose when it arrives.
17 strings: 5 for the call-permission-denied dialog plus 12 for the new
git repository overview / issues / patches screen and status pills.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`bridgeUrl` accepted the imeta `x` hash as a sufficient signal to route
a media URL through the local Blossom cache, even when the URL itself
didn't have the sha256 in its last path segment. For URLs like
https://i.nostr.build/M5AwJ.gif the upstream serves the blob under an
opaque filename, so the resulting `xs=https://i.nostr.build` hint sent
the local cache after https://i.nostr.build/<sha>.gif on miss — which
404s because the real blob isn't at that path.
Match the behaviour `bridgeProfilePictureUrl` already had: require
`extractSha256FromUrlPath(url)` to succeed before bridging. The imeta
hash is still preferred for canonical casing when present, but is no
longer enough on its own.
Add OnchainZapEvent handling to NotificationSummaryState so NIP-BC
onchain zaps targeting the current user contribute their claimed sats
to the daily zap total and chart alongside LnZapEvent receipts.
Lightning fields had moved entirely into the NWC wallet setup screen
(NIP47SetupScreen), so a user with no NWC connection had no way to set
their lud16/lud06 — required for receiving zaps. Add an expandable
Lightning section in the profile editor that reads/writes lud16 and
lud06, auto-expanded when either is set. NIP47SetupScreen keeps its
lud16 field; both flows go through sendNewUserMetadata which already
no-ops null params, so editing only one side never wipes the other.
https://claude.ai/code/session_01R2E6rMfhxKA8csVarjxKJd
OnchainSection now mirrors WalletCard's layout structurally so both
payment rails read as the same kind of object on the wallet screen,
with bitcoin-orange (BitcoinDark #F7931A / BitcoinLight #B66605)
replacing NWC's primary purple to keep them distinct at a glance:
- RoundedCornerShape(16.dp), 2.dp bitcoinColor border,
bitcoinColor.copy(alpha = 0.12f) container — same idiom NWC uses
for the 'default wallet' card.
- Header row: small orange ₿ chip + 'Bitcoin' title + 'Onchain ·
Taproot' subtitle on the left, big 24sp bold balance + 'sats'
label on the right (same typography as NWC).
- Loading shows an orange CircularProgressIndicator in the balance
slot. Error / no-backend states render an em-dash placeholder
with a small status caption.
- Address block: 'Your Taproot address' caption + monospace
truncated address.
- Action row: outlined 'Copy' with copy icon on the left, filled
bitcoin-orange 'Send' with send icon on the right — same 36dp
height + RoundedCornerShape(8.dp) as the NWC card actions.
OnchainZapSendDialog moves from AlertDialog to ModalBottomSheet —
matches the design language already used by CreateNestSheet,
EditNestSheet and the participant-action sheets. The form gets
imePadding + navigationBarsPadding, a scrollable content area, and a
sticky full-width Send button at the bottom.
Layout changes:
- Header with Bitcoin ₿ glyph + title + close.
- Recipient picker: the suggestion dropdown now sits flush under
its text field (was: separated by the outer Column's spacedBy gap).
- Amount section: big number field with 'sats' suffix + FlowRow of
SuggestionChips bound to AccountViewModel.zapAmountChoices (same
quick picks the LN zap dialog uses, formatted with showAmount).
- Fee priority: FlowRow of FilterChips with explicit vertical
padding around the row and inside each chip; each chip shows
'Slow / Normal / Fast', the sat/vB rate, and a rough ETA.
Selected chip uses bitcoinColor.
- State machine: idle → sending (orange spinner) → success
(bitcoinColor checkmark) / failure (error tint), each with a
'Done' / 'Close' bottom button.
Also wire RenderOnchainZap into ThreadFeedView.NoteMaster's event-type
dispatch right after RenderLnZap, so kind 8333 receipts get the full
rich render on the thread screen instead of the default text body.
Recipient field now uses ShowUserSuggestionList + UserSuggestionState
(same plumbing as the post composer @-mention and AwardBadgeScreen).
The user can type a display name, NIP-05, or paste an npub/hex; picked
recipients render as a chip with avatar + name + clear button. Raw
npub paste still works as a fallback when no result is picked.
Fee tier chips moved from Row to FlowRow so 'Slow · 1 sat/vB',
'Normal · 12 sat/vB', 'Fast · 50 sat/vB' wrap to a second line on
narrow dialog widths instead of clipping the rightmost chip.
observeEvents<LnZapEvent>(filterAcceptingBothKinds) is generic-erased
at runtime, so an OnchainZapEvent landing in the cache (e.g. right
after sending an onchain zap from the wallet) flowed through and
crashed in sumAmountsByUser's forEach checkcast.
Observe as <Event> and dispatch on type: LnZapEvent keeps its
private-zap decryption path; OnchainZapEvent attributes the
claimedAmountInSats to event.pubKey (no zap-request envelope).
Several callers pass Color.Transparent as the rich-text backgroundColor.
getGradient was copying that color and forcing alpha=1, which produced
opaque black (Color(0,0,0,1)) as the gradient end — so on a light theme
the fade behind the "Show more" button looked pitch black.
Fall back to MaterialTheme.colorScheme.background when the supplied
background has alpha 0, so the gradient fades from transparent into the
actual surface color.
Adds RenderOnchainZap, dispatched from RenderNoteRow alongside
RenderLnZap. The card shows a pulsing Bitcoin badge, sender→recipient
avatars, big sats amount in Bitcoin orange, an animated 'ON-CHAIN'
pill, a live confirmation pill (Verifying → In mempool → Confirmed at
block N) sourced from LocalCache.onchainBackend.getTx, and a
tap-to-copy mempool.space tx link.
Restores ReplyNoteComposition's parentBackgroundColor parameter (reverting
part of the previous commit). With calculateBackgroundColor now returning
the parent State directly when routeForLastRead is null, every inner
NoteCompose call site (replies, reposts, reactions, reports, approvals,
attestations, multi-set / message-set notification cards, and rich-text
nostr:event/nostr:note quotes) shares the level-0 bgColor State and fades
in lockstep with the outer note.
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.
When the user denied RECORD_AUDIO or CAMERA for a DM call, rememberCallWithPermission's
launcher callback only re-checked permissions and called onCall() on success — the denial
branch was empty. On the next button press Android skips the system dialog (permanently
denied) and immediately returns deny, so the call button appeared dead with no UI feedback.
Now the denial path opens an AlertDialog with an "Open settings" deep-link, matching the
NestActionBar pattern. Also splits the optional BLUETOOTH_CONNECT request onto its own
launcher so its result callback no longer triggers a second onCall().
calculateBackgroundColor captured parentBackgroundColor?.value once inside
remember(createdAt), so an inner NoteCompose (repost or reply preview) that
first composed while the outer was still highlighted would snapshot the
purple color and never observe the outer's fade back to the default
background.
- Repost inner notes now share the parent's bgColor State directly when
they have no own read-tracking, so they fade in lockstep with the outer.
- Reply previews no longer receive parentBackgroundColor at all; they rely
on replyModifier for their own visual style and never inherit the
purple "new item" highlight from the surrounding post.
GitHub's macos-13 runner image is being retired. Drop the x64 macOS
legs from both the desktop and CLI release matrices; macos-14 arm64
continues to ship the macOS builds.
- ktlint 1.8.0 changed how rule providers are loaded, and spotless 8.4.0 ends up handing the engine an empty ruleProviders set — so every file fails with IllegalArgumentException: A non-empty set of 'ruleProviders' need to be provided.
Measured the actual Kotlin daemon peak RSS during a full cold compile
of :amethyst:compilePlayDebugKotlin (which transitively compiles
:quartz, :commons, :quic, :nestsClient as well, in parallel after the
earlier parallel-mode change). Peak RSS was ~6.3 GB at the previous
12g/4g setting, meaning heap usage peaked around 4-5 GB plus
metaspace and native memory.
Drops to -Xmx8g -XX:MaxMetaspaceSize=2g, which:
- Cuts committed-virtual-memory ask from 18 GB (Gradle 6g + Kotlin 12g)
to 14 GB, comfortably under typical CI runner RAM and well under the
15 GB available in this dev environment.
- Frees ~4 GB of headroom for the OS file cache, which speeds up
classpath snapshot I/O on incremental builds.
- Leaves clear headroom over peak usage so GC pressure stays low; we
re-measured cold compile after the change at 3m30s vs 3m34s before,
well within run-to-run noise.
- Peak Kotlin daemon RSS at the new setting: 4.96 GB, confirming the
daemon right-sizes itself rather than pinning the ceiling.
If a much larger codebase target lands later (e.g. iOS framework
compilation in this same daemon), bump back up — the value is not
sacred, it just shouldn't request more than the host can afford.
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.
Auto-generated equals/hashCode would compare the ByteArray-list of
addresses by reference identity — a footgun no caller needs. No code
uses ==, copy, componentN, or hashing on records, so the class is now
plain with no synthesized methods.
The legacy `.json` reclaim was running in the constructor, which fires
on Application#onCreate (main thread) — a strict-mode regression vs the
prior no-op constructor. Moved into load(), which is documented as
background-only.
Also wraps the tmp-file write/rename in a try/finally so a writeRecords
crash partway (corrupt record, disk-full mid-write) or a copyTo
fallback can't leave an orphaned `.tmp` sibling behind.