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
- 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
Debuggable builds make ART run JIT-only and ignore the baseline
profile, which cripples the per-event hot path (JSON parse, hex
decode, SHA-256, JNI Schnorr verify per relay event). The staging
variant is non-debuggable with R8 like release, but debug-signed so
anyone can install it without the release keystore. It installs
alongside debug/release via the .staging applicationId suffix.
- 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