Welcome-screen branding pass to match the new launcher icon:
- Logo: swap ic_wisp_logo + the drawn radial-gradient disc for the
dark-wisp round mark on a solid black circle, delivered via a raster
drawable (ic_brand_logo). The mipmap can't be used directly because
R.mipmap.ic_launcher_round resolves to the adaptive-icon XML on
API 26+, which Compose's painterResource cannot render.
- Wordmark: restyle "dark wisp" from flat white to black text ringed in
an orange glow (brand orange #FF9800).
- Cancel (add-account flow): move out from under the logo/buttons
column, where it was painted over, to a visible pill in the top-start
corner (mirroring the Tor button at top-end).
Swap the inherited wisp launcher icons for the dark-wisp set: new
foreground artwork across all five density buckets (mdpi→xxxhdpi),
shipped as PNG. The adaptive-icon background moves from a vector
drawable to a flat black color resource (res/values/
ic_launcher_background.xml), matching dark-wisp's dark identity.
Removes the now-unreferenced ic_launcher_background drawable.
NsecPasteGuard existed but was only wired into MainActivity and a
handful of ProfileEditScreen/WalletScreen fields — most of the app's
text inputs (drawer status, list names, relay URLs, DM/group/compose
message boxes, safety mute words, search fields, custom emoji fields,
and the rest of the wallet screen) would silently accept a pasted nsec
instead of blocking it. Wire the guard into every remaining input,
matching wisp's existing full-coverage implementation, and cover
dark-wisp's local-relay tab (which wisp no longer has) too.
Previously the drawer's account registry (used by the account switcher
to show a name/avatar) was only updated once, during the active
account's startup sequence, via a one-shot snapshot check. A newly
imported key whose kind 0 hadn't arrived by that exact moment would
show a bare npub in the switcher forever, even after its profile
loaded moments later elsewhere in the app.
Now any fresh kind 0 for any account in the registry — not just the
active one — updates it immediately, from all three places a profile
event can land: live relay subscription, direct event cache, and
ObjectBox local seed.
Replace the inline expand/collapse account picker with a proper bottom
sheet (AccountSwitcherSheet), matching the sheet conventions already
used elsewhere in the app (e.g. ProfileQrSheet). The drawer header's
account affordance is now a single icon-only People glyph with a "+N"
badge for other signed-in accounts, instead of a count-dependent
icon-vs-chip toggle.
Accounts can now be reordered from the sheet via up/down buttons,
persisted through a new KeyRepository.moveAccount. The Anon Mode
toggle, which previously only rendered inside the account picker's
expanded state, now always renders in the drawer body since it's
unrelated to account switching.
Instant-zap configuration was buried in Settings → Interface. Move it into
the zap sheet where it's used, matching the Zap Cooking layout:
- ZapDialog section 7: when enabled, the toggle now shows the configured
amount + message and a "configure in Presets" hint; the bolt tints accent
when on.
- EditPresetsSheet: adds an "Instant zaps" toggle plus a per-row radio to
designate which preset is the instant amount. Done persists enabled state
and the selected preset's amount/message. Deleting the selected row moves
the selection safely.
- InterfaceScreen: remove the instant-zap enable/amount/message block. Fiat
mode's app-wide units toggle is untouched; the "Zaps" header is retained
for the remaining Zap Icon toggle.
Default stays off. Long-press instant zap continues to fire the configured
amount unchanged.
Hashtags containing accented or non-Latin letters (ä, ü, ö, é, ñ, etc.)
were truncated at the first non-ASCII character because the hashtag
character class was ASCII-only ([a-zA-Z0-9_-]). Switch to Unicode letter
(\p{L}) and combining-mark (\p{M}) property classes so the full tag is
parsed and highlighted as one token, and so composed notes tag it
correctly.
On-chain deposits sit unclaimed until explicitly claimed once they have
enough confirmations; the SDK emits SdkEvent.UnclaimedDeposits when a
deposit becomes claimable, and we weren't calling claimDeposit at all.
Now claims automatically on that event and once more on connect (in
case a deposit became claimable while the app was closed).
Also stops trusting PaymentStatus.PENDING for on-chain rows in general:
transactions made through another wallet app on the same seed aren't
tracked by this SDK session, so the status can stay stuck at PENDING
long after the underlying transaction is confirmed on-chain. The
mempool.space link already lets users verify actual confirmation
status for on-chain transactions.
Tapping the QR icon on the Receive sheet's lightning address row now
expands an inline QR card in place instead of navigating to a separate
page, and auto-scrolls it into view. Also stops the invoice-creation
spinner from appearing just because the shared isLoading flag happened
to be true when the sheet opened — it now only shows after the Create
invoice button is pressed.
Match the Transactions sheet treatment so Send and Receive overlay the
Home dashboard instead of replacing it, and keep the swipe-down-to-
dismiss gesture consistent across all three flows. Also pushes each
sheet's grab handle below the status bar so it doesn't get clipped by
the clock/camera cutout when the sheet expands to full height.
We are not supporting on-chain send/receive in-app. Detection and
display of on-chain transactions in history remains so deposits/
withdrawals made from another app on the same Spark seed still show
correctly (type, txId, mempool.space link).
Use PaymentDetails.Deposit/Withdraw to set isOnchain and surface the
real txId instead of the internal payment UUID. Set pending from
PaymentStatus.PENDING so the detail drawer shows accurate Status,
Type, and Transaction ID / mempool.space link for on-chain rows.
Tapping any row in the transaction history sheet expands an animated
detail panel showing Status, Type, Amount, Network fee, Date, Note,
and Payment hash with a one-tap copy icon. On-chain transactions get
a mempool.space deep-link. Pending transactions surface a "Pending"
badge before the timestamp.
Adds pending and isOnchain fields to WalletTransaction (both default
false; wire-up per backend can follow).
Send screen:
- Replace OutlinedTextField with tall BasicTextField in a dark card
(min 120dp, top-aligned placeholder, nsec guard on all paste paths)
- Scan QR · Paste · Gallery action row beneath the field (iOS parity)
- Accent-colored Next button with proper disabled state
- Spark hint now includes CLINK offer alongside Bitcoin address
Receive screen:
- Lightning address row: compact bolt icon + address + QR + copy icon
replacing the two-button Copy/Share card; QR icon navigates to the
existing LightningAddressQR page
On-chain send:
- Replace ArrowUpward direction indicator and button icons with the
AutoMirrored Send (paper airplane) icon on both Amount and Confirm
screens; remove redundant inline back buttons (TopAppBar handles back)
- Drawer header: show + circle (solo) or + N pill+chevron (multi) next to avatar for immediate account switcher access; remove redundant chevron beside username
- SplashScreen: accept onCancel param and render frosted Cancel pill when adding an account over an existing session
- Navigation: save previousAccountPubkey before navigating to splash, restore it fully on cancel (account switch + vm reload)
- AuthViewModel: add previousAccountPubkey field to support cancel-and-restore
The trimBlankLines pass was stripping \n\n before NostrProfileSegment and
HashtagSegment, treating them as block-level elements. They are inline —
they share a Text() composable with surrounding text and get no extra Column
spacing — so blank lines before them must be preserved.
Notes whose paragraphs start with a nostr: mention lost all paragraph
spacing on Android as a result.
REMOTE signing mode (NIP-55/Amber) has no private key on device.
Replace the reveal button with an OutlinedCard explaining that signing
is handled externally. READ_ONLY accounts get a plain text note.
LOCAL accounts are unaffected.
Removes share/analytics tracking params from URLs as they're rendered:
- utm_* and well-known click-ids (fbclid, gclid, msclkid, igshid, etc.)
on every host
- YouTube share params (si/feature/pp) scoped to YouTube hosts, covering
video, playlist, and channel links
Meaningful params (v, list, t, q, fragments) are preserved, and the URL
is returned untouched when nothing is stripped.
Replaces the search filter dropdown (ArrowDropDown button + menu) with a
two-tab segmented pill control (People | Notes, each with an icon) to match
iOS. The search field moves below the tabs into its own row with a leading
search icon and the Tune advanced-filter toggle alongside it.
Consolidates the profile's payment surfaces so each method appears once and
the layout is consistent.
- Action row: uniform 40dp circle buttons (send, zap, follow, mute); the zap
button routes straight to the zap composer
- Bio: long bios clip to ~5 lines with Read more / Show less; a CLINK noffer
in the bio renders as a compact "⚡ CLINK offer" label instead of the full
Pay-offer card (new plainNoffer flag on RichContent, scoped to the bio)
- Payment methods list: shared row layout with a fixed-width leading icon
column for alignment and 10dp tap padding for separation; raw addresses
(long Monero / noffer strings) dropped from the rows — the copyable value
lives in the sheet that opens on tap
- Lightning address is now tappable: zaps when a wallet is connected,
otherwise opens a QR + copy pay sheet (reuses PaymentTargetSheet)
- CLINK offer gets one explicit "Pay CLINK Offer" row (styled like the other
payment rows) instead of being duplicated across a button chooser, an
inline bio card, and a NIP-A3 target row
The "Uploading…" label was crammed into the compose toolbar's icon row
alongside six icon buttons, leaving no horizontal space — so the text
wrapped to three lines ("Uplo / ading / …"). Pull the spinner + label out
of the icon row into a dedicated line below it, wrapped in a rounded
surfaceVariant capsule with maxLines = 1 so it can never wrap. Animates
in/out with the upload lifecycle.
ExchangeRateRepository.init does disk I/O (loadCached) plus kicks off a network
refresh; move it off the main thread in WispApp.onCreate so it no longer gates
first frame. Rates populate asynchronously regardless and consumers already
handle the empty initial map. Startup-path win; matters more under GrapheneOS
exec-based spawning.
- RelayPool: replace per-event coroutine-per-signature-verify with a small
fixed worker pool fed by a bounded Channel. Reuses the RelayEvent already
built for _relayEvents so there's no extra per-event allocation; falls back
to an inline launch if the buffer is momentarily full so verification is
never skipped. Cuts thousands of coroutine+closure allocations/sec on the
hottest path (extra costly under GrapheneOS hardened_malloc).
- Event/RelayMessage: decode object-form events via fromJsonObject instead of
the generated serializer, removing one walk over the parsed tree per message.
- WispApp: baseline-profile-check executor is now a daemon and shut down after
its one-shot run instead of lingering for the app lifetime.
The bare `wallet_spark` string existed in all 10 translated locales but
not in the default locale and is not referenced in code (only
wallet_spark_title/_subtitle_recommended/_sdk are used). It tripped
lint's ExtraTranslation check, failing lintVitalRelease/Staging.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The feed publish path re-filtered the entire master feedList (capped at
5,000 events) on every 50ms settle window — a full toList() copy plus a
.filter{} pass where each event paid a Set lookup and an isRepostedByAny
LRU lookup, then a fresh list allocation and StateFlow emit. During an
inbound burst this ran up to ~20x/sec.
Maintain a parallel filteredFeed list incrementally instead: membership is
decided once at insert time via a single passesFilter() predicate, and all
feedList mutation sites (binaryInsert, repost re-sort, removeEvent,
purgeUser, purgeThread, resetFeedDisplay) update both lists in lockstep
under the same monitor. The settle/immediate handlers now just snapshot the
maintained list; the full O(n) filter pass survives only in
rebuildFilteredFeed() for the rare author/kind filter change. Inserts that
don't change the filtered view no longer trigger a feed emission, avoiding
redundant Compose recompositions.
Measured on a Pixel 9a (staging/R8, one-build A/B timing both paths over the
identical feedList snapshot per publish): at ~350 notes the publish path
dropped from median 167us / p90 243us to median 5.7us / p90 18us — ~23-29x
faster, and the gap scales linearly with feed size since the per-update
re-filter is eliminated.
Debug logging currently survives R8: RelayPool (51 Log.d calls) and
EventRepository (14) log in per-event paths, so release builds pay
string formatting and logd writes for every relay message, EOSE, and
poll/gallery event. -assumenosideeffects lets R8 remove the calls
along with their argument string building (verified: log strings
present in the dex before, gone after; Log.w/Log.e strings retained).
Debug builds are unminified and keep full logging.
- Replace stacked vertical lines with single L-shaped connector per non-root
reply: 12dp indent step capped at depth 5, 8dp corner radius, 1dp stroke,
suppresses PostCard full-width divider so no line extends left of the curve
- Sort thread children purely by created_at oldest-first; removes own-replies-
first bubbling to match iOS ThreadViewModel.buildNestedReplies
- Add ThreadReplyBar sticky bottom composable: 0.5dp divider + surfaceVariant
pill with "Reply…" placeholder and orange edit icon; tap opens compose with
the focal event as reply parent
- Gate Publish button on non-blank text or at least one uploaded attachment