The Blossom entry for nostrcheck.me was dropped in bcd2bc06
("Removes nip96 and updates Blossom recommendations", v1.06.0,
Feb 2026) along with the NIP-96 cleanup. This re-adds only the
Blossom endpoint.
The bundled Material Symbols font ships only the codepoints referenced
from MaterialSymbols.kt. Without regenerating the subset, newly added
icons render as tofu at runtime. Document this as a mandatory step in
CLAUDE.md so agents pick it up automatically.
Move the chip label, dialog title/body, and confirm button text out of
the composable and into strings.xml under wallet_onchain_public_* so they
can be translated via Crowdin.
Replace the vague "private channels" wording with concrete advice: fund
and drain via non-private accounts (e.g. exchanges), never mix with cold
wallets, and treat the balance as funds anyone with the nsec can spend.
- Extract PaymentTargetsDialog from PaymentButton so the reactions row
can render the same QR/Copy/Pay layout instead of the legacy two-row
M3ActionRow per target.
- PayReaction now subscribes via EventFinderFilterAssemblerSubscription
and observes the PaymentTargetsEvent reactively, so targets actually
load when the wallet icon is tapped.
- Correct the QrCode2 codepoint (U+E00A) and re-subset the bundled
Material Symbols Outlined font so the glyph is no longer a tofu box.
https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8
The onchain wallet's Taproot address is derived from the account's Nostr
pubkey, so anyone with the npub can see its balance and transaction
history on-chain. Surface that fact directly on the card with a tappable
"(i) Public" chip that opens a dialog explaining the privacy implication
and recommending private (non-Nostr) channels for funding and draining.
Each target now renders in a single row with the payment method and
address on the left and three trailing icon buttons: QR (opens a dialog
showing the raw address as a QR code), Copy (clipboard + toast), and
Pay (existing payto:// intent). Adds QrCode2 to MaterialSymbols.
https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8
srcDirs(vararg) on AGP 9 source-set Resources is deprecated; switch to
the directories MutableSet API. Preserves the original behavior of
including quartz/src/androidDeviceTest/resources in :benchmark's
androidTest java resources (verified bip39.vectors.json shows up in
processBenchmarkAndroidTestJavaRes output).
Converts the last four Groovy build scripts to Kotlin DSL so the entire
build is consistent with the rest of the modules (commons, quartz, etc.).
- settings.gradle -> settings.gradle.kts
- build.gradle (root) -> build.gradle.kts
- amethyst/build.gradle -> amethyst/build.gradle.kts
- benchmark/build.gradle -> benchmark/build.gradle.kts
Notes:
- Switched the per-subproject spotless format from groovyGradle to
kotlinGradle now that the only gradle scripts are .gradle.kts.
- benchmark module: moved targetSdk from defaultConfig to testOptions —
AGP 9 removed targetSdk from library defaultConfig.
- Reimplemented getCurrentBranch() with ProcessBuilder (Groovy's
String.execute() isn't available in Kotlin DSL).
Accounts whose reactionRowItems were persisted before ReactionRowAction.Pay
existed still load a 5-item list, so Pay never appears in Reaction Settings.
Add mergeWithDefaultReactionRowItems mirroring mergeWithDefaultVideoPlayerButtons
and apply it where reaction rows enter the StateFlow (constructor + updateFrom),
appending any default actions missing from the saved list while preserving
the user's existing order and toggles.
Capture the crashing thread's name from the UncaughtExceptionHandler and
surface it in the report (both in the device-info table and at the top of
the stack-trace block), so main-thread crashes can be distinguished from
coroutine-worker crashes at a glance.
The new Tune button inside the zap popup already opens the quick-zap
amounts editor, so the long-press shortcut from the zap button itself
(and from each chip inside the popup) is now redundant. Drop those
gestures and replace them with the custom-amount dialog that used to
live behind double-click:
- ZapReaction's button: long-press now opens ZapCustomDialog;
double-click is gone.
- Each chip in the popup: long-press now opens ZapCustomDialog
(via a new onCustomAmount callback threaded through the popup
overloads).
- NestActionBar's zap button wires onCustomAmount to its existing
wantsToSetCustomZap state.
- ReusableZapButton has no custom-zap dialog, so chip long-press
just dismisses the popup there.
The Tune button used the 40dp reactionBox with a 28dp icon, which was
visibly larger than the surrounding bolt chips. Use a compact 32dp box
with a 18dp icon so it lines up with the bolt glyph inside each chip.
The bolt-icon pill chips and the Tune settings button have different
intrinsic heights, so FlowRow's default top alignment leaves the
settings button visually higher than the chips. Set itemVerticalAlignment
so every item is centered within the row.
Media3's MediaSessionService.onStartCommand calls stopSelfSafely() when
delivered an intent (e.g. MEDIA_BUTTON from a headset) while no playback
is active. stopSelfSafely() invokes startForeground() to detach the
notification before stopping, which Android 12+ rejects when the app is
backgrounded with ForegroundServiceStartNotAllowedException.
Swallow that case in onStartCommand and stopSelf, matching the pattern
already used in NotificationRelayService.
https://claude.ai/code/session_01KTZDxK7zacrhFfqMHqt8dm
Wrap the zap-amount choices in the same elevated card / surfaceVariant
container used by the reactions popup, replace the flat primary-colored
material Buttons with pill-shaped Bitcoin-orange chips that pair a bolt
icon with the amount, and add a Tune action at the end of the row (the
zap equivalent of the AddReaction button in the reactions popup) so the
user can jump straight to editing their quick-zap amounts.
Use LocalConfiguration.current.locales[0] so the date formatter recomposes
when the device locale changes, satisfying the NonObservableLocale lint
check.
The Notifications feed could occasionally render as two stacked
LazyColumns — visible as ghosted text and "only the top one scrolls"
because both LazyColumns share the same `listState`. Reproduced by
double-tapping the bottom-nav Notifications button (each tap calls
`invalidateDataAndSendToTop(true)` → `clear()` + `refreshSuspended()`).
`RenderCardFeed` wrapped its `when (feedState)` in `CrossfadeIfEnabled`
→ `MyCrossfade`. `CardFeedState.Loaded` is a plain class with identity
equality, so back-to-back refreshes that transit through `Empty` or
`Loading` and back to `Loaded` inside the 100 ms animation can leave
two `Loaded` instances pinned in `currentlyVisible`. Each one composes
its own `FeedLoaded { LazyColumn(...) }`, both bound to the same
`LazyListState`, and only the topmost gets scroll input.
Switch directly on `feedState` with no crossfade — the animation was
only 100 ms and barely perceptible. Other CrossfadeIfEnabled callers
are untouched.
The remember(...) call returned Unit, which tripped the
RememberReturnType lint. Use LaunchedEffect, which is the
idiomatic Compose API for keyed side effects.
Two things turned out to be the same bug:
1. New calendar events arriving from relays didn't show up in either
feed without a manual pull-to-refresh. The screen was stuck on
whatever the last full refresh saw.
2. After a top-nav filter switch the feed looked like it was reflecting
"a previous state" — same root cause: filter switch ran a full
refresh from LocalCache, but events that the new subscription
subsequently delivered were dropped on the floor.
AccountFeedContentStates.updateFeedsWith / deleteNotes had calls for
every other feed but neither calendarAppointmentsFeed nor
calendarCollectionsFeed — so LocalCache.live.newEventBundles flowed
past them. Added both calls (and the matching deleteFromFeed entries)
so new appointments/collections insert into the visible feed live.
Also dropped the redundant "Calendar Lists" label above the top-nav
filter spinner on the collections screen — it duplicated the screen
title shown by the navigation chrome.
When loadRelayInfo was invoked from a main-thread Compose coroutine and
the call got cancelled, okhttp's executeAsync cancellation handler
closes the Response on the resuming dispatcher (AndroidUiDispatcher).
Closing the response drains any unread bytes from the SSL socket, which
triggers a blocking read on the main thread and throws
NetworkOnMainThreadException — surfaced to the user as
CompletionHandlerException.
The previous withContext(Dispatchers.IO) was nested inside the .use {}
block, so neither the initial suspension at executeAsync() nor the
implicit response close in the cancellation handler ran on IO. Moving
the dispatcher switch to wrap the entire function ensures the
continuation is dispatched on IO and the cancellation/close path runs
there too.
Self-audit of the previous MLS reply commit surfaced four concrete
improvements:
1. Push-notification cold-start replies now thread reliably. The
receiver previously rebuilt the parent inner event by looking it up
in LocalCache; in a cold-process broadcast that cache hasn't been
re-hydrated yet (Account.restoreAll runs async on init), so the
q-tag silently dropped. Carry the parent's inner event id AND
author pubkey through the Intent extras and feed them straight to
MarmotManager.buildTextMessage, which now takes (eventId, author)
instead of a full Event. The cold-start reply is now always
threaded, not just the warm-cache case.
2. marmotGroupRelays() is no longer duplicated. The receiver was
reimplementing what AccountViewModel had as a private fun (which
itself was used 9× inside the VM). Lifted to Account so headless
callers can reach it without spinning up a ViewModel; both sites
now share one implementation.
3. Dropped the dead editFromDraft / draftId plumbing. Added for route
symmetry with NIP-17 but Marmot has no draft persistence, so the
parameter rode all the way through MarmotGroupChatView only to be
ignored under @Suppress("UNUSED_PARAMETER"). Per CLAUDE.md, don't
pre-emptively abstract; reinstate when drafts actually land.
4. MarmotGroupMessageComposer no longer has default-param `remember`
blocks. There's exactly one caller and it always passes both
messageState and replyTo — the defaults were just noise.
The in-chat send path also captures (id, pubKey) under the replyTo
guard before launching the send coroutine, so a slow send + a user-
cleared reply state can't race into a partially-threaded message.
No protocol or behavioral change for the warm-cache happy path; the
threading improvement is observable only on cold-start push-reply.