Commit Graph
1323 Commits
Author SHA1 Message Date
Barry DeenandGitHub 2dd0239e21 chore: bump version to 1.2.0 (#623) 2026-07-20 10:34:11 -04:00
19193b1268 feat(wallet): transaction history sheet, Send/Receive as bottom sheets, Receive redesign (#607)
* fix(wallet): correctly detect on-chain Spark transactions in history

PaymentDetails.Deposit and PaymentDetails.Withdraw are now recognized as
on-chain payments; their txId is used as the payment hash instead of the
UUID payment ID. Adds pending and isOnchain fields to WalletTransaction.

* feat(wallet): expandable transaction detail drawer in history sheet

Tapping a row in the full transaction history sheet expands an inline
detail panel showing status, type (on-chain/Lightning), amount, network
fee, date, note, and payment hash with a copy icon. On-chain transactions
get a "View on mempool.space" link. Compact home-screen list is unchanged.

* feat(wallet): transactionsChanged flow, makeInvoice expiry, bitcoin: prefix handling

WalletProvider interface gains transactionsChanged SharedFlow (emitted on
payment succeeded/failed/pending) and makeInvoice(expirySecs: Int = 3600).
SparkRepository also calls refreshBalanceInternal() on PaymentPending.
NwcRepository wires expirySecs through to Nip47 MakeInvoice.
Nip47 serializes optional expiry field per NIP-47.
WalletViewModel: setSendAmount(), processInput strips bitcoin:/BITCOIN:
prefix, generateInvoice threads expirySecs through to the provider.

* feat(wallet): Send/Receive as bottom sheets, Receive redesign, inline address QR

Send, Receive, and Transactions each present as full-page ModalBottomSheets
over the Home dashboard (swipe down to dismiss), matching iOS behavior.
WalletSheetDragHandle pushes the grab handle below the status bar so it
clears the camera cutout on full-height sheets.

Receive screen redesign: expiry selector (1 h / 24 h / Custom) replaces
the Invoice/Address tab row. Lightning address shown as a compact row below
the Create invoice button with bolt icon, address, QR toggle, and copy.
Tapping the QR icon expands an inline QR card within the sheet and
auto-scrolls to it; tapping again collapses. Loading indicator only appears
after the Create invoice button is pressed, not on sheet open.

Send screen redesign: tall dark-card BasicTextField replaces OutlinedTextField;
Scan / Paste / Gallery action row replaces OutlinedButton pair; accent-colored
Next button with disabled state.

Strings: wallet_receive_expires_*, wallet_receive_via_address,
wallet_send_input_hint_onchain, wallet_tx_pending, action_copy,
wallet_copy_address.

* fix(wallet): correctly settle on-chain payments made from another Spark wallet on the same seed

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.

---------

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
2026-07-20 10:14:18 -04:00
29fc418abf feat(thread): collapsible reply threads with inline expand + iOS-style reply attribution (#622)
Long/deep reply threads were hard to follow — replies piled up at a stuck
indent past a few levels, with no clear signal of who was replying to
whom. Replies deeper than 3 levels now fold behind a "Show N more replies"
affordance; tapping expands the whole hidden subtree in place, holding the
viewport on the note above the button.

- Typed ThreadItem model (Post / CollapsedReplies / ShowMoreReplies) and a
  pure, unit-tested ThreadFlattener replace the flat Pair<NostrEvent, Int>
  list, shared by note threads and article comments.
- Shared threadIndentDp/threadConnector draw a single L-shaped depth-guide
  rail per reply instead of stacked vertical lines; orphaned rails dash
  their top to signal they continue upward.
- Sticky "Reply…" bar targets the note at the top of the viewport, not
  always the root.
- Reply order is purely oldest-first (dropped bubbling the user's own
  replies to the top of each level).
- Reply attribution ("Replying to X") moves above the commenter's row,
  de-linked and in secondary color, matching the iOS layout. NIP-05 badge
  hidden on reply rows.
- Pluralized "1 more reply" / "N more replies".

Not in this change: fan-out "show more replies" for wide threads
(parameterized, disabled), infinite-scroll-on-expand, and Publish-button
gating (tracked separately).

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
2026-07-20 10:13:44 -04:00
Nicola SuscaandGitHub b5b76cfebc test(nostr): unit test Blossom.buildServerListTags (#614)
* chore(nostr): fix formatting

* fix(nostr): reject empty Blossom server list tags

* chore(nostr): document `Blossom.buildServerListTags`

* test(nostr): unit test `Blossom.buildServerListTags`

Covers the builder's current contract: it rejects an empty server
listand maps already-validated server URLs into BUD-03 server tags.
Invalid URL strings are **intentionally** not tested here because URL
validation is owned by the repository/viewmodel boundary before values
reach this helper.
2026-07-15 12:51:21 -04:00
Nicola SuscaandGitHub 0ba989f5c6 refactor(nostr): use Java Base64 for Blossom auth headers (#616)
Replace `android.util.Base64` with `java.util.Base64` for Blossom auth
headers. This facilitates easier testing of `Blossom.createUploadAuth`
by removing the need for android instrumentation or roboelectric tests.
2026-07-15 12:50:52 -04:00
82c2101b6c feat(drawer): account switcher as a modal sheet + reordering (#620)
* feat(drawer): account switcher as a modal sheet + reordering

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.

* fix(accounts): sync account registry for any known account's kind 0

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.

---------

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
2026-07-15 12:50:32 -04:00
56c26c238a feat(zap-sheet): iOS-layout rewrite + instant-zap settings + post-card long-press + pin Send (#574)
* feat(zaps): instant-zap settings — InterfacePreferences + Interface UI

Adapted from feat/one-tap-zap commit 9ec4fea, with the AppSettingsRepository
sync wiring stripped out (NIP-78 cross-device sync of these prefs is
deferred to a future phase).

InterfacePreferences:
  • isQuickZapEnabled / setQuickZapEnabled
  • getQuickZapAmountSats / setQuickZapAmountSats (clamped 1..10K)
  • getQuickZapAmountFiat / setQuickZapAmountFiat
  • getQuickZapMessage / setQuickZapMessage
  • QUICK_ZAP_MAX_SATS = 10_000L

InterfaceScreen — new "Zaps" / "Payments" section between Fiat Mode and
the Zap Icon toggle, exposing the four settings.

No UI wiring yet for the long-press behavior or the in-sheet toggle —
those come in subsequent commits in this branch.

* feat(zap-sheet): instant-amount seed, in-sheet toggle, caps, confirmation, friendly errors

Ports the behavioral half of iOS commit #4 from feat/one-tap-zap.
The full layout rewrite (hero amount, recipient row, FlowLayout
preset strip, privacy dropdown, hidden TextField with 450ms focus
deferral, register-style cents on every keystroke, EditPresetsSheet,
scroll-dismisses-keyboard) is deferred — the existing ZapDialog
layout still works and the new behaviors are the user-facing
material change.

What landed:

  • **Instant-amount seed on open.** First mount with no
    initialSatsHint pre-fills `customAmount` from
    `interfacePrefs.getQuickZapAmountSats()` (or the fiat
    equivalent in cents), and `message` from
    `interfacePrefs.getQuickZapMessage()`. Treats the configured
    instant-zap amount as the "preferred opening amount" even when
    quick zaps are disabled — matches iOS.

  • **In-sheet Instant-zaps toggle.** New row above the action
    buttons, bound directly to `InterfacePreferences.isQuickZapEnabled`
    so flipping it from the sheet propagates to the post-card
    long-press behavior (and the NIP-78 backup) without navigating
    to settings. Label flips with fiat mode.

  • **1,000,000-sat hard cap.** Zap button disables and a red
    "Max 1,000,000 sats per zap" caption surfaces above the action
    row when the effective amount crosses the cap. Hard cap, not
    a confirmation.

  • **10,000-sat soft confirmation.** Below 10K the Zap button
    fires immediately. At/above 10K it routes through an
    AlertDialog ("Zap N sats? — This is a large amount, double-check
    before sending") with Send / Cancel. Below the cap so users
    can recover from a stray preset tap.

  • **`friendlyZapErrorMessage()` utility.** Mirrors iOS's
    `ZapAnimationStore.friendlyMessage(for:)` substring-match table
    plus the Swift-enum description fallback (extracts `("…")`
    when present). Internal so post-card error pills + future
    layouts can both call it.

* feat(post-card): tap composer / long-press instant zap + self-zap disabled

Ports iOS commit #5 from feat/one-tap-zap. Splits the zap-glyph
gesture into two paths and renders the button as disabled for the
user's own posts.

ActionBar:
  • New optional `onZapLongPress: (() -> Unit)?` parameter — null
    means "no long-press behavior, tap-only" (existing call sites
    keep working without changes).
  • Zap glyph switches from IconButton to a Box with
    `combinedClickable`, supporting onClick (open composer) AND
    onLongClick (fire instant zap). When `onZapLongPress` is null,
    the long-press handler is omitted entirely so the glyph behaves
    exactly as before.
  • `longPressFired` flag pinned in remember{} — Compose, like
    SwiftUI, fires both onClick AND onLongClick on release of a
    long-press, so the tap handler short-circuits the second fire
    when the flag is set.
  • Disabled tint moved from 0.4f to 0.35f opacity to match the
    iOS self-zap rendering.

PostCard:
  • Plumbs `onZapLongPress` through to ActionBar.
  • Self-zap disabled: `zapEnabled = zapEnabled && !isOwnEvent`
    so the user's own posts render the glyph at low opacity AND
    both tap + long-press are short-circuited (the long-press
    handler returns null when zapEnabled is false in ActionBar).

What's NOT in this commit:
  • The actual "instant zap fires the configured amount" wiring at
    call sites (RichContent's WispActions etc.). The gesture
    infrastructure is in place; plugging it in requires reaching
    into ZapSender / WalletViewModel and is a separate, larger
    commit that touches every call site that constructs WispActions.

* feat(zap-sheet): full iOS-layout rewrite + ModalBottomSheet for drag-dismiss

Previous pass only added behaviors (instant-amount seed, in-sheet
toggle, caps, confirmation). The layout still used a centered
Dialog that filled the screen and offered no dismiss gesture —
"too tall and impossible to dismiss". This commit rebuilds the
composer from scratch to match the iOS reference screenshot.

Container: switch from `Dialog` to `ModalBottomSheet`. Gives you:
  • Drag handle at the top (Material3 supplies it).
  • Swipe-down dismiss + scrim-tap dismiss.
  • Partial-height presentation so the sheet doesn't take over
    the whole viewport.

Layout (top to bottom, mirrors iOS spec §2.6 of the port doc):
  1. **Toolbar** — "Close" pill on the left, orange-tinted
     "Presets" pill on the right (opens the Save-preset dialog).
  2. **Recipient row** (when `recipientPubkey` + `profileLookup`
     are provided) — 32dp avatar, display name + lud16 stacked,
     trailing copy-icon button that pushes the lud16 to the
     clipboard. Hidden gracefully when no profile data is wired.
  3. **Hero amount** — 56sp orange rounded-bold number with a
     muted-orange unit caption ("sats" or fiat code) underneath.
  4. **Preset strip** — wrapping FlowRow of pills. Last chip is
     `Custom` with an inline + badge that saves the current
     amount as a new preset (disabled at 8-preset max or when
     the amount already exists).
  5. **Custom amount field** — inline OutlinedTextField, only
     visible when the Custom chip is selected. Digit-only.
  6. **Message field** — single-line OutlinedTextField with
     "Message (optional)" placeholder. Preset taps auto-fill
     their default message only when the field is blank, so a
     mid-type tap doesn't clobber what the user wrote.
  7. **Privacy dropdown** — single-row pill with eye / eye-slash
     / lock icons and helper subtext. Material3 DropdownMenu
     opens on tap. Hidden when `forcePrivate` is on.
  8. **Instant zaps toggle** — bound to the existing
     `interfacePrefs.isQuickZapEnabled` setting (and therefore
     to the NIP-78 sync). Flipping it here propagates without
     re-opening Interface settings.
  9. **Zap button** — full-width, accent fill, white bolt + sats
     copy. Disabled when amount is 0 or over the 1M hard cap. At
     >10K it routes through the existing soft-confirmation alert.

Stripped:
  • LightningBackground (decorative animated dots).
  • AnimatedBoltHeader (centered pulsing bolt).
  • drawMiniBolt + bespoke ZapPresetChip / ZapChipButton scaffolding.
None of these matched the iOS reference; the new layout is simpler
and reads cleaner at a glance.

Signature kept compatible — added two optional params
(`recipientPubkey`, `profileLookup`); existing callers keep
working, the recipient row just hides. Wired the FeedScreen and
thread Navigation.kt call sites to pass profile data; remaining
call sites (groups, DM, profile, hashtag, set-feed, article,
notifications) still compile but won't show the recipient row
until they're updated.

* fix(zap-sheet): pin Zap button above keyboard + wire recipient on every call site

Two follow-ups to the ZapSheet rewrite:

1. **Zap button no longer hides behind the keyboard.** The previous
   layout put the Zap button at the end of a single Column inside
   the bottom sheet, so when the amount field focused, the
   keyboard pushed the whole content up and the button went
   off-screen with no scroll to reach it.

   Restructured the sheet body into two rows: a scrollable upper
   region (toolbar, recipient, hero, presets, message, privacy,
   instant-zaps toggle) holding `weight(1f, fill = false)` and a
   pinned lower region with the cap warning + Zap button. The
   outer Column gets `imePadding()` so the whole stack floats
   above the IME — button stays visible, upper region scrolls if
   the keyboard cuts into it.

2. **Recipient row now renders on every call site.** Only 3 of
   ~12 ZapDialog call sites were passing `recipientPubkey` +
   `profileLookup` in the rewrite commit — the row hid silently
   on the other 9. Wired the rest:
     • Navigation.kt — search, hashtag feed, set feed, article,
       live stream (uses streamer override pubkey when set),
       notifications (post + DM target).
     • FeedScreen — zap-poll target.
     • UserProfileScreen — post zap (eventRepo lookup) AND
       profile-direct zap (embedded profile shortcut).
     • DmConversationScreen — uses the peerProfile already in
       scope.

* fix(interface): apply imePadding so the keyboard doesn't cover input fields

The Interface settings screen's scroll column had no IME inset handling,
so any text field that takes focus (most visibly the new instant-zap
Amount / Fiat / Message inputs in the Zaps section, but also any future
input on this screen) got covered by the soft keyboard.

Adds Modifier.imePadding() between .padding(padding) and the
verticalScroll. With the IME inset accounted for, the scroll viewport
shrinks to fit above the keyboard and the focused field auto-scrolls
into view as Compose normally handles.

---------

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
2026-07-04 15:45:20 -04:00
Nicola SuscaandGitHub cf0a157674 test(nostr): improve and unit test Blossom.parseServerList (#613)
* chore(test): use version catalog for test deps

* refactor(nostr): require event kind 10063 in `Blossom.parseServerList`

`Blossom.parseServerList` currently has a single caller:
`BlossomRepository.updateFromEvent`. That caller already ignores
non-kind 10063 events before invoking the method, so the added `require`
does not change repository behavior.

The method already assumed kind 10063 input; by making this explicit,
the requirement is documented, testable, and prevents future callers
missing it.

* feat(nostr): ignore invalid urls in `Blossom.parseServerList`

`Blossom.parseServerList` returns a list of advertised servers which
must be valid urls as per BUD3.

* feat(nostr): return distinct blossom server urls

`Blossom.parseServerList` returns distinct server urls parsed from the
events tags. Duplicated list items are not useful and can lead to bugs
or unnecessary overhead if not anticipated.

* chore(nostr): document `Blossom.parseServerList`

* test(nostr): unit test Blossom.parseServerList
2026-07-04 15:38:30 -04:00
Barry Deen bb0a196cc0 Merge PR #582: feat/video-loop-setting 2026-07-04 15:21:35 -04:00
2c0bd634ad fix(content): parse hashtags with non-ASCII Unicode letters (#611)
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.

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
2026-07-04 15:08:29 -04:00
Barry DeenandGitHub f5f7b3ef01 Merge pull request #610 from barrydeen/fix/dm-date-separator-placement
fix(dm): place date separators above whole day-group in reversed list
2026-07-01 13:08:43 -04:00
Barry Deen d307f8f8e3 fix(dm): place date separators above whole day-group in reversed list
The conversation list uses reverseLayout with newest-to-oldest iteration,
but emitted each day's date header right after the group's newest message.
Because reverseLayout inverts emission order on screen, the divider landed
just above the single newest message, pushing all older same-day messages
up under the previous day's header. Only the latest message showed below
its correct divider.

Emit each day's header at the group's older boundary (using a lookahead to
the next, older message) so reverseLayout renders it on top of the whole
day-group.
2026-07-01 13:07:30 -04:00
Barry DeenandGitHub 2c12f8a852 Merge pull request #609 from barrydeen/fix/profile-notes-feed-pagination
fix(profile): keep notes feed paginating and render inline gallery media
2026-07-01 12:50:29 -04:00
Barry Deen df2f81fb3d fix(profile): keep notes feed paginating and render inline gallery media
The profile Notes tab degraded into gallery-only posts after scrolling past
recent notes, those gallery posts never loaded their media, and there was no
infinite scroll.

- Only surface kind 20/21/22 in the chronological notes feed when they arrive
  via the time-bounded, paginated userposts/userposts-more stream. The separate
  unbounded usergallery sub (limit 100, no time bound) now feeds galleryPosts
  only, so it no longer walls off the feed with far-back gallery events or
  poisons oldestNoteTimestamp (which broke loadMoreNotes' until anchor).
- Render gallery kinds with GalleryCard (imeta via Nip68/Nip71, Coil) instead
  of PostCard, which only extracts URLs from content text.
- Auto-load more on scroll for the Notes and Replies tabs (RECENCY sort only),
  not just the Media tab.
2026-07-01 12:49:43 -04:00
Barry DeenandGitHub 089f652c28 Merge pull request #603 from dmnyc/fix/linebreaks-before-mentions
fix(richcontent): preserve blank lines before inline profile/hashtag segments
2026-06-30 17:31:11 -04:00
Barry DeenandGitHub 5ab7ccf94a Merge pull request #604 from dmnyc/feat/multi-account-switcher
feat(accounts): multi-account switcher + cancel add-account flow
2026-06-30 17:30:50 -04:00
Barry DeenandGitHub 2fa2ff22ea Merge pull request #605 from dmnyc/feat/tx-history-sheet
feat(wallet): transaction history as swipe-up bottom sheet
2026-06-30 17:29:57 -04:00
The Daniel ea6d03b296 feat(wallet): transaction history as swipe-up bottom sheet
The Transactions page now overlays the home dashboard as a
ModalBottomSheet rather than replacing it. Swipe up 40px on the
recent transactions footer or tap "View all" to open; swipe down
or tap the scrim to dismiss.
2026-06-30 09:00:00 -04:00
The Daniel 4ba3b13cd2 feat(accounts): add/switch account affordances + cancel add-account flow
- 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
2026-06-27 17:44:55 -04:00
The Daniel 7e37a952f2 fix(richcontent): preserve blank lines before inline profile/hashtag segments
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.
2026-06-27 00:00:39 -04:00
Barry DeenandGitHub f4224ecdc3 Merge pull request #602 from Letdown2491/feat/strip-link-trackers
feat: strip tracking params from links in note content
2026-06-26 14:10:22 -04:00
Letdown2491 4d80d24f10 feat: strip tracking params from links in note content
Removes share/analytics tracking params from URLs as they're rendered:
- utm_* and well-known click-ids (fbclid, gclid, msclkid, igshid, and more) 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.
2026-06-25 22:08:57 -06:00
Barry DeenandGitHub af643e1404 Merge pull request #599 from barrydeen/chore/bump-version-1.1.2
Bump version to 1.1.2
2026-06-18 12:43:21 -04:00
Barry Deen 7cba3deaf6 Bump version to 1.1.2 2026-06-18 12:42:58 -04:00
Barry DeenandGitHub 6016d4053a Merge pull request #573 from dmnyc/feat/zap-bolt-glow-pulse
feat(bolt): white-core glow pulse for in-flight zap
2026-06-18 12:33:11 -04:00
Barry DeenandGitHub 2a6455f35e Merge pull request #594 from dmnyc/fix/compose-uploading-pill
fix(compose): move uploading indicator to its own line as a capsule pill
2026-06-18 12:31:11 -04:00
Barry DeenandGitHub 685227453c Merge pull request #557 from dmnyc/fix/collapse-zap-spam
fix(zaps): collapse same-actor zap spam in notifications and post drawer
2026-06-18 12:21:55 -04:00
Barry Deen 747a10b996 Merge remote-tracking branch 'origin/main' into fix/collapse-zap-spam
# Conflicts:
#	app/src/main/kotlin/com/wisp/app/ui/component/ReactionDetailsSection.kt
2026-06-18 12:20:42 -04:00
Barry DeenandGitHub 32d3fa3434 Merge pull request #572 from dmnyc/feat/drop-relay-backup-status-list
fix(wallet): drop per-relay backup status list from Spark settings
2026-06-18 12:16:37 -04:00
Barry DeenandGitHub 72c0293029 Merge pull request #580 from dmnyc/feat/wallet-receive-redesign
feat(wallet): iOS-parity Receive screen — text input + note + invoice / address tabs
2026-06-18 12:14:00 -04:00
Barry DeenandGitHub 66a05a0a33 Merge pull request #563 from dmnyc/feat/wallet-nwc-backup
feat(wallet): cross-platform NWC connection backup via NIP-78 + parity polish
2026-06-18 12:10:30 -04:00
Barry DeenandGitHub 1755d4c7c4 Merge pull request #598 from barrydeen/feat/music-track-card
feat: render music-track (kind 36787) as inline playable card
2026-06-18 12:09:22 -04:00
Barry Deen 3b7ca349d8 feat: render music-track (kind 36787) as inline playable card
A kind-1 note embedding a nostr:naddr reference to a music track (kind
36787) previously fell through to the generic "Unsupported event kind"
placeholder. This adds a dedicated, playable card.

- RichContent.kt: dispatch kind 36787 to a new MusicTrackCard composable
  that resolves the addressable event via the existing kind-generic
  findAddressableEvent/requestAddressableEvent path and renders artwork,
  a MUSIC chip, title, artist, and an author row.
- The play/pause control is wired into the global AudioPlayerController,
  so playback survives row recycling and drives the floating mini-player
  plus lock-screen/notification controls. Shows a live scrubber while it
  owns the player, the static duration tag otherwise.
- EventPersistence.kt: persist kind 36787 so seen tracks resolve
  instantly on warm restart.

Ports wisp-ios #378 to Android. Single tracks only; the music-tracks
collection kind (30817) stays on the generic placeholder.
2026-06-18 12:08:41 -04:00
Barry DeenandGitHub 188a584c2b Merge pull request #590 from Letdown2491/fix/spam-score-anr
fix: compute spam score off-lock to prevent input-dispatch ANR
2026-06-18 11:57:00 -04:00
Barry DeenandGitHub f699fd0f93 Merge pull request #591 from Letdown2491/feat/staging-build-type
feat: add staging build type for release-performance on-device testing
2026-06-18 11:55:47 -04:00
Barry DeenandGitHub a4160a18d8 Merge pull request #597 from barrydeen/perf/feed-filter-and-spam-lock
perf: incremental feed filter + off-lock spam scoring
2026-06-18 11:55:18 -04:00
Barry Deen 4a5e382590 perf: incremental feed filter + off-lock spam scoring
Ports two performance fixes from the dark-wisp-android fork (PRs #34, #23).

EventRepository: maintain a parallel filteredFeed incrementally instead of
re-filtering the entire feedList (up to 5,000 events) on every 50ms publish
window. Membership is decided once at insert time via a single passesFilter()
predicate; the consumer loops now just snapshot the maintained list. The full
O(n) pass survives only in rebuildFilteredFeed() for the rare filter change.
Inserts that don't change the filtered view no longer trigger an emission,
avoiding redundant Compose recompositions during inbound bursts.

NotificationRepository: hoist regex-heavy spam classification out of
synchronized(lock). addEvent now calls warmSpamScore() before the lock, which
applies the same gating as mergeReply and populates SpamAuthorCache; inside the
lock mergeReply only consults the cache. Prevents the main thread from blocking
past the 5s input-dispatch ANR deadline during notification bursts.
2026-06-18 11:54:23 -04:00
Barry DeenandGitHub 6c817accef Merge pull request #596 from barrydeen/fix/chat-bubble-subcompose-crash
fix: avoid intrinsic measurement of chat bubble content (SubcomposeLayout crash)
2026-06-18 11:42:43 -04:00
Barry Deen e9cdb7f579 fix: avoid intrinsic measurement of chat bubble content (SubcomposeLayout crash)
Opening a group room or DM whose visible messages contained a media
carousel crashed with:

  IllegalStateException: Asking for intrinsic measurements of
  SubcomposeLayout layouts is not supported.

The message-bubble content Column used Modifier.width(IntrinsicSize.Max),
which forces an intrinsic-width measurement of every child. One child is
RichContent, which can render MediaCarousel (a BoxWithConstraints +
HorizontalPager). Those are SubcomposeLayout-based and cannot answer
intrinsic queries, so the measure pass threw.

Remove IntrinsicSize.Max from the group and DM bubble columns and let
the bubble hug its content. The received-message header row no longer
fills width (which would otherwise balloon every bubble to max width
without the intrinsic sizing); the sender name uses widthIn(max) so long
names still ellipsize.
2026-06-18 11:42:12 -04:00
Barry DeenandGitHub f74ec95d44 Merge pull request #595 from barrydeen/fix/event-relays-concurrent-modification
fix: prevent ConcurrentModificationException in eventRelays set
2026-06-18 11:37:04 -04:00
Barry Deen f51a6adeb1 fix: prevent ConcurrentModificationException in eventRelays set
eventRelays stored per-event relay URLs in plain LinkedHashSet instances
(mutableSetOf). addEventRelay mutates these live sets from relay IO threads
as events stream in, while getEventRelays returns the same live set that UI
coroutines and getRelayHintsForEvents iterate on the Main dispatcher. A
concurrent add mid-iteration threw ConcurrentModificationException
(LinkedHashMap$LinkedKeyIterator.next).

Use ConcurrentHashMap.newKeySet(), whose iterator is weakly consistent, so
concurrent adds during iteration no longer throw. This matches the existing
pattern already used for the sibling repostAuthors cache.
2026-06-18 11:34:23 -04:00
The Daniel aaf952233e fix(compose): move uploading indicator to its own line as a capsule pill
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 / …"). This pulls the spinner +
label out of the icon row onto a dedicated line below it, wrapped in a
rounded surfaceVariant capsule with maxLines = 1 so it can never wrap.
It animates in/out with the upload lifecycle.

Mirrors barrydeen/dark-wisp-android#38.
2026-06-17 12:23:54 -04:00
Barry DeenandGitHub 9ea046ef78 Merge pull request #592 from Letdown2491/fix/lazy-inline-video-players
fix: lazily create inline video players to survive media-heavy notes
2026-06-12 14:52:00 -04:00
Letdown2491 9fafa1f20c fix: lazily create inline video players to survive media-heavy notes
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
(such as a 155-video note seen in the wild) instantly spawned a
player per URL -- 156 players / 487 player threads measured upstream,
exhausting hardware codec instances (~16-32 device-wide) and memory
until input dispatch timed out (ANR).

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.

Port of barrydeen/dark-wisp-android#31.
2026-06-12 12:14:40 -06:00
Letdown2491 15261fba91 fix: compute spam score off-lock to prevent input-dispatch ANR
NotificationRepository.addEvent ran NSpamClassifier.score() inside
synchronized(lock) via mergeKind1 -> mergeReply. Feature extraction
runs several regex passes over up to ten notes per author; doing it
under the lock stalls every caller -- including main-thread callers
like getAllPostCardEventIds() and markRead -- past the 5s
input-dispatch deadline.

Fix: warmSpamScore(event) runs before the lock, computes the score
under the same spamFilterEnabled/isFollowing/safelist/reply-target
guards, and caches it in SpamAuthorCache (LruCache-backed, thread-
safe). mergeReply now only consults the cache.

Port of barrydeen/dark-wisp-android#23.
2026-06-12 10:47:02 -06:00
Letdown2491 7d08cc879f feat: add staging build type for release-performance on-device testing
Non-debuggable + R8-minified like release (initWith(release)), but
debug-signed so anyone can install it without the release keystore.
applicationIdSuffix ".staging" and app name "Wisp Staging" let it
install alongside debug and release builds.

debuggable=true makes ART ignore the baseline profile and run
JIT-only with deoptimization support, which cripples the per-event
hot path (JSON parse, hex decode, SHA-256, JNI Schnorr verify per
relay event) -- so debug builds can't be used to judge real
performance.

Port of barrydeen/dark-wisp-android#24.
2026-06-12 10:47:02 -06:00
Barry DeenandGitHub 04253bbcb1 Merge pull request #586 from dmnyc/fix/wallet-self-send-tx-order
fix(wallet): keep self-send legs distinct + deterministic tx order
2026-06-12 12:29:55 -04:00
The Daniel ff47689aaf fix(wallet): keep self-send legs distinct + deterministic tx order
A self-payment surfaces as two transaction records sharing one payment
hash — one outgoing, one incoming. Port the iOS WalletStore.dedupTransactions
hardening so they render correctly:

- Add dedupTransactions(): drop exact (paymentHash, type) repeats the
  backend returns, and sort newest-first with the incoming "received" leg
  above its outgoing "sent" leg on a timestamp tie. Apply on initial load,
  re-enrich, and load-more.
- Key the transaction LazyColumn by "paymentHash|type" so the two legs of a
  self-send keep distinct identities and both render (mirrors iOS
  WalletTransaction.id).
2026-06-06 15:36:00 -04:00
Barry DeenandGitHub 5175babef0 Merge pull request #585 from barrydeen/fix/lint-extra-translations
fix: remove stale wallet translation keys blocking release build
2026-06-05 17:33:19 -04:00
Barry Deen 23b11104c3 fix: remove stale wallet translation keys blocking release build
Six wallet string keys (wallet_connect_wallet, wallet_choose_how,
wallet_create_new, wallet_create_description, wallet_nwc_description,
wallet_spark) were renamed in the default locale but left behind in
10 translation files, causing 60 ExtraTranslation lint errors that
failed lintVitalRelease.
2026-06-05 17:30:12 -04:00