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.
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
InlineVideoPlayer and InlineVideoPlayerWithFullscreen created a fully
prepared ExoPlayer per video URL the moment it entered composition,
with no cap or visibility gating. A note carrying many video URLs
(e.g. the circulating 155-video stress-test note) instantly spawned a
player per URL: 156 players / 487 player threads / 1800 process
threads measured on device, exhausting hardware codec instances
(~16-32 device-wide) and memory until input dispatch timed out (ANR)
in STACK media layout.
The player now only exists while its video is near the viewport:
created when >50% visible with autoplay on, or on tap (which starts
playback immediately); released with its position remembered once the
video scrolls fully off-screen. Until then the slot renders the
uploader-provided NIP-92 imeta "image" preview frame, falling back
to the existing thumbhash/blurhash painter. parseImetaTags now parses
the "image" entry, and MediaCarousel video tiles use it too instead
of a blank box when no thumbhash/blurhash is present.
Verified on device against the 155-video note: peak 7 ExoPlayer
threads (was 549), no ANR, playback/mute/fullscreen/PiP unaffected.
- MediaCarousel: horizontal swipe gallery for multi-image posts with
page indicators
- FullScreenMediaPager: swipe between images in full screen
- ZoomableAsyncImage: pinch-zoom, pan, double-tap and swipe-down dismiss
- Media layout setting (gallery/stacked) in Interface preferences
NIP-A3 payment targets re-grafted into the new sheet layout: target
chips render in the scrollable column, the targets-only dialog still
covers the no-wallet/no-lud16 case, and all call sites pass both the
new profileLookup and the NIP-A3 params. LocalCanSign gate from wisp's
watch-only mode intentionally dropped (no watch-only in dark-wisp).
- Register-style fiat amount entry in the zap dialog with live sats
conversion via ExchangeRateRepository
- Zap messages render image URLs inline in the engagement drawer,
collapsed to [image] on the top banner (new ZapMessageImage util)
- Redesigned zap rows in reaction details as mini-posts via RichContent
- Adds String.toNpub() helper used by the new zap row fallbacks
Strips Keys.deriveSparkEntropy, SparkRepository.generateDefaultFromPrivkey/
isDefaultWallet/normalizeMnemonic, WalletViewModel isDefaultWallet state,
OnboardingViewModel auto-derive call, and all WalletScreen UI branches that
keyed on isDefaultWallet. Also adds String.toNpub() extension (Nip19) and
removes stale toNpub imports that were breaking the build.
Most of #570 (default-wallet primary row + More-options accordion)
already arrived via the #563 parity port; this adds the remaining
WalletPrimaryRow treatment for the Spark row on the mode picker.
Paying a noffer required a local private key (KeyRepository.getKeypair),
so sessions signed in with an external signer (e.g. Amber) were told to
sign in. NofferClient now takes a NostrSigner and routes NIP-44
encrypt/decrypt and event signing through it; both call sites build the
right signer for the active signing mode (local key, remote signer, or
read-only -> error).
- NIP-69-style noffer parsing (Noffer.kt) + encrypted RPC client
(NofferClient) for fetching invoices from CLINK services
- Full-height NofferPaySheet with payee recognition and amount entry
- noffer1 strings in notes, profiles, QR sheet and drawer resolve to a
pay action; onPayInvoice plumbed through DM, group & search notes
- Offer field on profile edit; payee re-parse on profile cache update
- NofferClient routes through the relay client provider (Tor-aware)
- Publishes the NWC connection string as an encrypted NIP-78 app-data
event on connect, replacing any prior backup
- Restores the saved connection when reconnecting on a new device
- Relay backup hidden for default Spark wallets (nsec is the canonical
backup); kept for non-default seeds
- Dashboard footer lists the 5 most recent transactions instead of 1
- Delete wallet replaced by a switch-wallet flow: disconnect to use the
default wallet or restore another; copy clarifies funds stay safe
- NWC dashboard parity + wallet settings cleanup
- Spark settings no longer render the per-relay backup status list
- Default Spark wallet deterministically derived from the user's nsec
(HKDF seed derivation), with onboarding auto-setup and default-wallet
backup banner
- Two-tier connect screen: Spark vs NWC chooser with colored buttons
- Wallet dashboard parity for both Spark and NWC: expandable wallet
info card, lightning address row, transaction history polish
- Balance display cycles sats / fiat / hidden
- NWC connect screen redesigned to match iOS: paste/scan split card,
connection hint, close pill
- NsecPasteGuard blocks pasting an nsec into connection/seed fields