100 Commits
Author SHA1 Message Date
Barry DeenandGitHub 11ac08f7da chore: bump version to 1.2.1 (#627) 2026-07-23 10:47:39 -04:00
Barry DeenandGitHub e109818ab8 fix: guard media placeholder decode against non-positive dim values (#626) 2026-07-23 10:42:26 -04:00
Barry DeenandGitHub 2dd0239e21 chore: bump version to 1.2.0 (#623) 2026-07-20 10:34:11 -04:00
Barry Deen bb0a196cc0 Merge PR #582: feat/video-loop-setting 2026-07-04 15:21:35 -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
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
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
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
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
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
Barry DeenandGitHub fa82ac2a1a Merge pull request #583 from barrydeen/feat/wot-filter-threads
feat: apply web of trust filter to thread replies
2026-06-05 17:17:09 -04:00
Barry Deen 2e19a69249 chore: bump version to 1.1.1 2026-06-05 17:14:58 -04:00
Barry Deen 198313e6b3 feat: apply web of trust filter to thread replies
The WoT filter was already silently dropping events at EventRepository
ingestion and in NotificationRepository, but thread replies bypassed
both paths — ThreadViewModel keeps its own threadEvents map seeded via
cacheEvent() and getCachedThreadEvents(), neither of which checks WoT.

Apply isWotFiltered in rebuildTree() alongside the existing block /
mute / spam filters. The root note is always shown (user explicitly
navigated to it); replies from authors outside the qualified network
are silently dropped, matching how WoT behaves in the feed and
notifications.
2026-06-05 17:09:30 -04:00
Barry DeenandGitHub a7020603d2 Merge pull request #566 from dmnyc/fix/spark-default-wallet-banner
fix(wallet): refresh Spark display state on mnemonic swap + banner copy
2026-05-25 12:59:28 -04:00
Barry DeenandGitHub 14f237b6a1 Merge pull request #559 from dmnyc/feat/zap-with-image
feat(zap): render zap messages as mini-posts in the engagement drawer
2026-05-25 12:58:49 -04:00
Barry DeenandGitHub b0a9eb379d Merge pull request #561 from dmnyc/fix/nwc-connect-screen-ios-parity
fix(wallet): NWC connect screen — iOS 1:1 redesign
2026-05-25 12:58:01 -04:00
Barry DeenandGitHub bdef0b2748 Merge pull request #565 from dmnyc/feat/android-dark-mode-backgrounds
feat(theme): iOS-parity dark-mode polish for Android
2026-05-25 12:57:43 -04:00
Barry DeenandGitHub 6416a8353e Merge pull request #562 from dmnyc/feat/wallet-balance-toggle
feat(wallet): cycle balance display between sats, fiat, and hidden
2026-05-25 12:57:21 -04:00
Barry DeenandGitHub 6833fba03e Merge pull request #555 from dmnyc/feat/profile-ux-polish
feat(profile): circle action buttons, tighter tabs, quoted note navigation
2026-05-20 10:47:04 -04:00
Barry Deen bd5fa0d684 Merge branch 'main' into feat/profile-ux-polish 2026-05-20 10:46:14 -04:00
Barry DeenandGitHub d0ff54d530 Merge pull request #554 from dmnyc/feat/search-segmented-tabs
feat: segmented pill tabs on search screen
2026-05-20 10:41:48 -04:00
Barry DeenandGitHub 1dc4ef0cfe Merge pull request #553 from dmnyc/feat/nsec-paste-guard
feat: nsec paste guard — block accidental private key pasting app-wide
2026-05-20 10:39:06 -04:00
Barry Deen 895c105848 Merge branch 'main' into feat/nsec-paste-guard
Resolved conflicts in Navigation.kt, AuthScreen.kt, and ComposeScreen.kt
to combine the nsec paste guard with the CompositionLocalProvider
read-only signing wrapper, QR scanner on AuthScreen, and the compose
gallery placeholder change.
2026-05-20 10:36:46 -04:00
Barry DeenandGitHub 1fe7f219a0 Merge pull request #552 from dmnyc/feat/qr-scan-watch-only-mode
feat(login): QR scan + watch-only mode for npub/nprofile
2026-05-20 10:32:36 -04:00
Barry Deen 493102d762 Merge remote-tracking branch 'origin/main' into feat/qr-scan-watch-only-mode
# Conflicts:
#	app/src/main/kotlin/com/wisp/app/ui/component/WispDrawerContent.kt
2026-05-20 10:31:47 -04:00
Barry DeenandGitHub ab9c31be57 Merge pull request #551 from dmnyc/feat/update-nostr-icon
feat(auth): swap Nostr ostrich icon for new pixel logo
2026-05-20 10:28:52 -04:00
Barry DeenandGitHub d4b5881e59 Merge pull request #550 from dmnyc/feat/wallet-derivation-test-vectors
test(wallet): cross-platform parity spec + Spark derivation test vectors
2026-05-20 10:28:34 -04:00
Barry DeenandGitHub 30b9966115 Merge pull request #549 from dmnyc/feat/wallet-dashboard-parity
feat(wallet): dashboard parity — top bar, welcome banner, pill, inline tx
2026-05-20 10:25:58 -04:00
Barry Deen e39e1f7943 Merge remote-tracking branch 'origin/main' into feat/wallet-dashboard-parity
# Conflicts:
#	app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt
2026-05-20 10:25:25 -04:00
Barry DeenandGitHub 33dd703235 Merge pull request #548 from dmnyc/feat/wallet-connect-two-tier
feat(wallet): two-tier Connect screen + Spark sub-screen
2026-05-20 10:20:22 -04:00
Barry DeenandGitHub 68a959a31d Merge pull request #547 from dmnyc/feat/wallet-icon-credit-card
feat(wallet): universal credit-card wallet icon
2026-05-20 10:19:30 -04:00
Barry DeenandGitHub b476dcda24 Merge pull request #546 from dmnyc/feat/compose-countdown-progress
feat(compose): countdown progress bar + auto-scroll preview
2026-05-20 10:18:29 -04:00
Barry DeenandGitHub 572aad73b6 Merge pull request #545 from barrydeen/chore/google-client-id-update
chore: update Google OAuth web client ID
2026-05-17 13:25:58 -04:00
Barry Deen df425d0dde chore: update Google OAuth web client ID 2026-05-17 13:25:32 -04:00
Barry DeenandGitHub 9a553b3ee6 Merge pull request #544 from barrydeen/chore/google-client-id-v1.1.0
chore: update Google OAuth client ID and bump to 1.1.0
2026-05-17 13:15:49 -04:00
Barry Deen f56e16d421 chore: update Google OAuth client ID and bump to 1.1.0 2026-05-17 13:15:02 -04:00
Barry DeenandGitHub 110f651a86 Merge pull request #543 from barrydeen/feat/private-reactions-zaps
feat(private-replies): gift-wrapped reactions + DIP-03 zaps on private replies
2026-05-17 12:50:49 -04:00
Barry Deen 426d9353cd feat(private-replies): gift-wrapped reactions + DIP-03 zaps on private replies
Re-enable React and Zap on NIP-17 private replies as private-by-default flows.
Reactions are kind-7 rumors gift-wrapped to every conversation participant + a
self-copy with k=1 to distinguish from existing DM reactions. Zaps reuse the
existing DIP-03 pipeline against the rumor id/created_at, locked private at the
ZapDialog so a public fallback can't leak the rumor id. Repost and Quote remain
hidden — they would publicly attach an e-tag pointing at the encrypted rumor.

- New PrivateReactionPublisher fans out to rumor.pubkey + p-tag participants
  with DM relay + NIP-65 inbox fallback, optimistic local insert dedups against
  the relay-echoed self-copy via deterministic rumor id.
- EventRouter.processGiftWrap dispatches kind-7 rumors by their k tag:
  k=1 routes through eventRepo.addEvent so counts and notifications surface
  alongside public reactions; k=14 (or absent) stays on the DM path.
- SocialActionManager auto-routes reactions and zaps based on
  eventRepo.isPrivate(event.id); v1 is add-only — toggle-off via gift-wrapped
  NIP-09 deletion is a future enhancement.
- ZapDialog gains forcePrivate which hides the anon/private toggles and holds
  isPrivate=true. ActionBar gains zapEnabled so the thread can gray out the
  zap button (with a toast) when the recipient lacks DM relays or the user is
  on a remote signer.
- FlatNotificationItem.isPrivateReaction joins isPrivateReply/isPrivateZap;
  the lock-icon check now ORs all three (retroactively surfaces the lock on
  private zap notifications which had the flag populated but not rendered).
- Rename eventRepo.isPrivateReply -> isPrivate (and markPrivateReply ->
  markPrivate) since the set now tracks any gift-wrap-materialised event.
2026-05-17 12:49:53 -04:00
Barry DeenandGitHub 3ef4b14ee7 Merge pull request #542 from barrydeen/feat/nip05-root-domain-display
feat(nip05): drop `_` from root-domain identifiers on display
2026-05-17 12:17:20 -04:00
Barry Deen 877ef1c20e feat(nip05): drop _ from root-domain identifiers on display
Per NIP-05, a local part of `_` denotes the root domain. Render
`_@domain.com` as `@domain.com` everywhere we display a NIP-05
identifier. Verification continues to use the raw stored value.
2026-05-17 12:02:41 -04:00
Barry DeenandGitHub 66f1020f47 Merge pull request #541 from barrydeen/feat/dip03-private-zaps
feat(zaps): private zaps via DIP-03 + DM-relay routing
2026-05-17 11:57:57 -04:00
Barry Deen eeeca6f7ef feat(zaps): private zaps via DIP-03 + DM-relay routing
Migrate the homegrown DM-relay-routed plaintext zap scheme to DIP-03
(damus-io/dips/03.md) for sender anonymity, layered on top of DM-relay
receipt routing for amount/recipient privacy. Each layer covers the
other's failure modes:

- LNURL respects `relays` + both sides have auth-gated DM relays →
  no sender, recipient, amount, target, or message visible publicly.
- LNURL leaks to public relays → sender stays hidden via the anon tag.
- A "DM relay" turns out to serve reads unauthed → amount leaks to
  that relay's subscribers, sender still hidden.

Protocol:
- Nip04: raw (ct, iv) encrypt/decrypt so callers can package bytes
  in non-standard envelopes.
- Nip19: bech32 codec promoted to internal for DIP-03 anon-tag use.
- Nip57: buildPrivateZapRequest (deterministic ephemeral key from
  sha256(privkey + targetId + createdAt), inner kind 9733 signed
  by real sender, NIP-04 encrypted to recipient, bech32-packed
  as `pzap1...<ct>..._iv1...<iv>...` into the outer kind 9734
  `anon` tag, outer signed by ephemeral). decryptPrivateZap
  (recipient path), decryptOwnOutgoingPrivateZap (re-derives the
  ephemeral and ECDHs against the outer `p` tag for self-attribution
  + own-message recovery), isPrivateZap (anon-tag presence check).
  Inner kind 9733 Schnorr signature verified before trusting it.

Send path:
- ZapSender: isPrivate branch builds via Nip57.buildPrivateZapRequest
  and routes `relays` tag to (ourDmRelays + recipientDmRelays) only.
  Fails fast if no DM relays available on either side.
- SocialActionManager: thread eventCreatedAt through; subscribe for
  receipt on our DM relays so NIP-42 AUTH gates reads to us.

Receive path:
- EventRepository.resolveZapSender: try recipient-decrypt with our
  privkey; fall back to self-attribution by re-deriving the ephemeral
  for the target note; final fallback to outer pubkey/content. Used
  for ZapDetail construction, WoT filtering, wallet-history sender
  map, and the `isOwnOptimistic` dedup against optimistic entries.
- isPrivate detection: Nip57.isPrivateZap (anon-tag presence) in
  EventRepository and NotificationRepository, replacing the legacy
  "all relay-tag URLs are my DM relays" heuristic.
- All zap-sender extraction sites (NotificationRepository,
  EventRouter, Article/ThreadViewModel) route through
  resolveZapSender.

UI:
- FeedViewModel.hasLocalKeypair: one-shot check; remote-signer
  accounts can't sign/decrypt under DIP-03, so private toggle is off.
- Navigation: canPrivateZap = hasLocalKeypair && our DM relays &&
  recipient DM relays. Live-stream surface explicitly false
  (a-tag addressable events have no concrete note id for the
  ephemeral derivation).

Cleanup:
- Drop the now-unused EventRepository.dmRelayUrls field and its
  3 assignment sites; detection moved off the heuristic entirely.
- Old DM-relay-routed zaps from prior Wisp builds no longer style
  as private; they render as normal public zaps (sender + message
  were already plaintext, so no info loss).
2026-05-17 11:56:50 -04:00
Barry DeenandGitHub 20b9425817 Merge pull request #540 from barrydeen/feat/private-replies
Private replies via NIP-17 gift wrap
2026-05-17 10:12:00 -04:00
Barry Deen 20504a5d5c fix(private-replies): fall back to recipient inbox relays, not our own
When the recipient hasn't published a kind 10050, fall back to their
NIP-65 inbox (read) relays — fetching the kind 10002 list fresh from
indexers if we haven't seen it yet — instead of silently dropping the
wrap on our own write relays where the recipient never queries.

Drop the prior write-relays / own-write-relays fallback chain entirely:
neither is an inbox the recipient is listening on, so a wrap landing
there was guaranteed lost. If we can't resolve any inbox we return
sentCount=0 and surface the existing "no relays connected" error.

PeerRelayListLookup is a small shared helper for the kind 10002 fetch.
2026-05-17 10:09:52 -04:00
Barry Deen 4e98611712 Revert "fix(private-replies): timestamp range, filter bypass, fresh relay-list fallback"
This reverts commit 118eff35c8.
2026-05-17 10:05:54 -04:00
Barry Deen 118eff35c8 fix(private-replies): timestamp range, filter bypass, fresh relay-list fallback
Three independent reasons a private reply could go missing, all addressed:

- randomizeTimestamp now picks in the past up to 2 days, matching the
  NIP-59 recommendation. The earlier 1-day cap was a defensive workaround
  for clients with tight since-filters on kind-1059 subscriptions; that
  constraint no longer holds in practice.
- NotificationRepository was silently dropping private-reply
  notifications when the sender was outside the recipient's web-of-trust
  or scored as spam by the classifier. Both filters now check
  eventRepo.isPrivateReply and skip — a gift-wrapped reply is explicit
  and addressed to the recipient, so spam/WoT gating is wrong here.
- PrivateReplyPublisher only consulted cached kind 10002 if the
  recipient had no kind 10050; if neither was available we fell back to
  our own write relays, which the recipient never queries. Now fetches
  kind 10002 fresh from indexers via the new shared PeerRelayListLookup
  helper before falling back. DmConversationViewModel.fetchPeerRelayList
  delegates to the same helper.
2026-05-17 10:04:00 -04:00
Barry Deen 14a26662fb feat(compose): shorten toggle toasts to "<Label> ON/OFF"
Replace the verbose explainers with the actual state change: "NSFW ON",
"Mining OFF", "Private Reply ON" etc. Reads cleaner and the user can
see the new state at a glance instead of parsing a sentence.

The toggle reads the inverse of the collected state to derive the new
value (StateFlow update hasn't propagated to the local var at toast
time). Locked private toggles short-circuit in the VM, so we force ON.
2026-05-16 19:16:51 -04:00
Barry Deen daa4ff98b4 feat(compose): toggle explainer toasts + swap private icon to VisibilityOff
- Tapping NSFW / Proof of work / Private reply in the compose toolbar now
  fires a short Toast describing what the toggle does. Fires on every tap
  (whether enabling or disabling) so the explanation is consistent.
- Replace Icons.Outlined.Lock with Icons.Outlined.VisibilityOff for the
  private-reply indicator in compose, thread, and notifications. Reads as
  "hidden from public" — closer to the actual semantic than a padlock.
2026-05-16 19:09:42 -04:00
Barry Deen 8b137cf063 feat(private-replies): notif quick-reply, lock icon, PoW on rumor
- Extract send into PrivateReplyPublisher so both the full compose screen
  and the notifications inline-reply share the same recipient relay
  resolution, self-copy, optimistic insert, and PoW path.
- Notifications quick-reply detects when the parent is a private reply we
  received (via EventRepository.isPrivateReply) and routes through the
  gift-wrap path; otherwise falls through to the existing public publish.
- Replace the mask-style ic_private_zap with Icons.Outlined.Lock for the
  private-reply indicator in compose / thread / notifications. The
  private-zap row indicator is unchanged.
- Mine PoW on the kind 1 rumor before wrapping when the user has Note
  PoW enabled. The committed nonce + difficulty travel inside the
  encrypted wrap and the recipient renders the standard PoW badge after
  decryption. Mining is dispatched on Dispatchers.Default so the UI
  thread never blocks.
2026-05-16 18:56:15 -04:00
Barry Deen 7018be95dc feat(private-replies): hide noisy actions and chain privacy
- ActionBar gates React / Repost / Quote / Zap behind !isPrivate, leaving
  Reply and Bookmark on private replies. Avoids leaking the rumor id via
  a public e-tag on kind 7 / 6 / 9735 attached to the rumor.
- ComposeViewModel auto-enables and locks the private toggle when the
  parent being replied to is itself a private reply. Sending publicly
  would attach an e-tag to the rumor id on public relays and leak the
  thread structure, so the toggle is forced on and ignores clicks.
2026-05-16 18:36:52 -04:00
Barry Deen ecf5b367b4 feat(compose): private replies via NIP-17 gift wrap
Adds a reply-only "private" toggle in the compose toolbar. Enabling it
gift-wraps the reply (kind 1 rumor inside kind 1059) and routes it to the
recipient's DM relays plus a self-copy to the sender's own DM relays — no
public kind 1 is published. Private replies surface in the recipient's
notifications and the thread view with an orange lock indicator that
mirrors the existing private-zap badge.

Inbound is handled by the existing kind 1059 / "dms" subscription, so no
new REQs are needed. The Nip17 unwrap allowlist is widened to accept
kind 1 rumors; EventRouter synthesizes a local NostrEvent from the rumor,
marks it private on EventRepository, and dispatches through the normal
reply notification + thread BFS paths.

Recipient DM-relay lookup is extracted into DmRelayLookup so the new
compose path and the existing DM send share the same indexer query,
4-second collection window and LRU cache.
2026-05-16 17:45:40 -04:00
Barry DeenandGitHub 330ad80634 Merge pull request #539 from barrydeen/feat/nsec-derived-default-wallet
feat(wallet): derive default wallet from nsec during onboarding
2026-05-15 23:26:23 -04:00
Barry Deen 7d2bf59b99 feat(wallet): derive default wallet from nsec during onboarding
The new-user onboarding flow was still generating a random BIP39 mnemonic
in startDiscovery before connecting Spark, so the wallet for a brand-new
account could not be restored by signing in with the same nsec on another
device. Switch to generateDefaultFromPrivkey so the default wallet is
deterministic from the user's key.

Also drop the NIP-78 relay backup for default wallets: the mnemonic is
recoverable from the nsec, and Breez retains the lightning address
registration server-side, so there is nothing extra to persist. Manual
backup of non-default (random) wallets is unchanged.
2026-05-15 23:25:32 -04:00
Barry DeenandGitHub 1b4e250679 Merge pull request #538 from barrydeen/feat/google-backup-hardening
feat(auth): require recovery PIN and strip identifying metadata from Drive backups
2026-05-15 22:37:24 -04:00
Barry Deen 5029fbface feat(auth): require recovery PIN and strip identifying metadata from Drive backups
Google-account-only custody meant anyone with the Google login could
decrypt the nsec; the filename leaked the npub to Drive; the chooser's
profile prefetch told relays which npubs were on this device.

- Derive the backup key from PBKDF2-HMAC-SHA256(PIN, salt=HMAC(sub))
  with 600k iterations. PIN is a 4–8 digit numeric set during sign-in
  with a confirm step; mismatch and wrong-PIN paths surface inline.
- Pull `sub` from the signed ID token's JWT instead of
  GoogleIdTokenCredential.id (which is the email, not stable across
  Workspace renames).
- Use opaque `wisp_bk_<uuid>.bin` filenames and recover the npub by
  decrypting. Drop the delete-then-upload race since there's no longer
  a replace path.
- Seed the chooser's profile REQ with 10 decoy pubkeys pulled from a
  popular relay so observers can't pick the real backups out of the
  query.
2026-05-15 22:34:22 -04:00
Barry DeenandGitHub acadead68b Merge pull request #537 from barrydeen/feat/live-stream-keep-screen-on
feat(live): keep screen awake while watching a live stream
2026-05-15 21:52:10 -04:00
Barry Deen 912da5e959 feat(live): keep screen awake while watching a live stream
Add FLAG_KEEP_SCREEN_ON to the host activity window while
LiveStreamScreen is composed, mirroring the behavior already used by
the fullscreen video player. The flag is cleared on dispose so it
does not leak to other screens.
2026-05-15 21:51:32 -04:00
Barry DeenandGitHub f4da67ac99 Merge pull request #536 from barrydeen/feat/splash-continue-with-nostr
feat(auth): Continue with Nostr splash button + password manager autofill
2026-05-15 16:15:47 -04:00
Barry Deen 237efec2f4 feat(auth): collapse splash buttons into Continue with Nostr with key autofill
Replaces the separate Create Account / Log In buttons on the splash with a
single purple-ostrich Continue with Nostr button. Tapping opens a bottom
sheet to either paste an existing nsec/npub or generate a new account.

New accounts trigger a CredentialManager save-password prompt so the device
password manager can store the nsec under the npub. Tapping the input field
fires a one-shot getCredential request, letting the password manager fill
the field from previously saved keys.
2026-05-15 16:15:07 -04:00
Barry DeenandGitHub aa552fc3a8 Merge pull request #535 from barrydeen/fix/google-drive-401-refresh
fix(auth): refresh stale Drive access token on 401 and re-prompt for consent
2026-05-15 15:31:40 -04:00
Barry Deen b999aebd42 fix(auth): refresh stale Drive access token on 401 and re-prompt for consent
After a user revokes Wisp's authorization in their Google account
settings, Play Services may still hand back the previously-issued
access token from its local cache. The Drive API then 401s and sign-in
fails with no way for the user to recover from inside the app.

Detect the 401, clear the stale token from Play Services' cache via
GoogleAuthUtil.clearToken, and re-call AuthorizationClient.authorize().
With no cached token, authorize() contacts Google's servers, sees the
revoked consent, and returns a resolution PendingIntent — surfacing the
consent dialog so the user can re-grant the drive.appdata scope. Once
they consent, we retry listBackups with the fresh token.

  - DriveBackupService throws DriveAuthorizationExpiredException on 401
    (instead of a generic IOException) and exposes the stale token so
    callers can pass it to clearToken
  - GoogleSignInManager.refreshDriveAccessToken(activity, staleToken):
    clears via GoogleAuthUtil and re-runs getDriveAccessToken (which
    handles the resolution PendingIntent the same way as initial
    sign-in)
  - GoogleAuthViewModel.listBackupsWithRefresh wraps the initial list
    call with a single retry on the expired-auth exception
2026-05-15 15:28:34 -04:00
Barry DeenandGitHub 58c08a0f9a Merge pull request #534 from barrydeen/fix/google-encrypted-drive-backup
fix(auth): restore encrypted Drive backup; drop deterministic sub derivation
2026-05-15 15:09:08 -04:00
Barry Deen 977da039df fix(auth): restore encrypted Drive backup; drop deterministic sub derivation
#530 derived the Nostr private key as SHA-256(sub || index). The Google
sub claim is not a secret — any app the user signs into with Google
gets the same sub value for that account/OAuth-client pair, and even
across clients it leaks via id_token introspection. That meant any
third party with the sub could regenerate the user's nsec offline. A
non-starter.

Reverts to the #528 design:

  - GoogleSignInManager re-acquires the drive.appdata OAuth scope via
    AuthorizationClient
  - DriveBackupService reads/writes wisp_nsec_<npub>.bin blobs in the
    per-app appDataFolder (other apps cannot see this folder)
  - BackupCrypto already provided HMAC-SHA256(sub)-derived key + NIP-44
    encryption; that file was not touched by #530 and is reused as-is
  - GoogleAuthViewModel lists backups, surfaces a chooser with avatar +
    display name fetched from kind-0 events, and decrypts only on
    explicit Restore
  - "Create new account" generates a fresh keypair, encrypts, and
    uploads a new blob

The encryption key is still derived from sub, but the attack surface
is fundamentally different: an attacker now needs both the sub AND
read access to Wisp's appDataFolder in the user's Drive. The folder
is sandboxed per-app, so the only paths in are the user's own Google
account or a Wisp app compromise. That's the same trust boundary as
"can sign in to the user's Google account."

Splash button styling kept as-is (dark variant from #530 retained — a
cosmetic choice independent of the security model).

Reverts: 3d3c6e1, de88bdd
2026-05-15 15:02:45 -04:00
Barry DeenandGitHub 6133b10b6a Merge pull request #533 from barrydeen/refactor/remove-local-relay
refactor: remove local relay functionality
2026-05-15 11:44:07 -04:00
Barry Deen 463240c16d refactor(res): remove local_relay strings; tighten cleartext to false
Drops all local_relay_* keys from English and 10 translated locales.
Sets cleartextTrafficPermitted=false in the network security config
now that ws:// is no longer needed for any relay URL.
2026-05-15 11:36:32 -04:00
Barry Deen 77ec550571 refactor(ui): remove LOCAL tab and feed relay-picker entry
RelayScreen drops the LOCAL tab and its LocalRelayTab composable.
FeedScreen's RelayPickerDialog no longer surfaces a "My Local
Relay" row, and its localRelayUrl parameter is removed.
2026-05-15 11:36:25 -04:00
Barry Deen 94445d3086 refactor(repo,vm): drop local relay persistence and viewmodel state
Removes the local_relay SharedPrefs entry, KeyRepository's
localRelayFlow / save/get/load helpers, and RelayViewModel's
local-relay state and update methods. StartupCoordinator no longer
wires the relay pool to localRelayFlow. reloadPrefs clears the
orphaned local_relay key on account switch.
2026-05-15 11:36:18 -04:00
Barry Deen 008d533ac5 refactor(relay): drop LocalRelayConfig and local relay runtime forwarding
Removes LocalRelayConfig, LocalRelayWritePolicy, the LOCAL value in
RelaySetType, and isLocalRelayUrl from RelayConfig.kt. Strips local
relay state, forwarding paths, lifecycle pause/resume, and the
own-notes OutboxRouter branch from the relay layer.
2026-05-15 11:36:10 -04:00
Barry DeenandGitHub f640e02912 Merge pull request #532 from barrydeen/feat/spark-nsec-derived-default-wallet
feat(wallet): derive default Spark wallet from user's nsec
2026-05-15 11:23:15 -04:00
Barry Deen f2eda5f7aa feat(wallet): derive default Spark wallet from user's nsec
New users get a Spark wallet auto-created on first wallet-tab entry, deterministically derived from their nsec via HKDF-SHA256. The nsec is the only backup needed — signing in on another device re-derives the same wallet. The seed-phrase backup gate and relay-backup warnings are skipped for default wallets; the recovery phrase remains viewable in settings for cross-app export. Disconnecting lands the user on a setup screen with three explicit options: use the default wallet, restore from recovery phrase, or restore from relays. Existing custom-wallet users are unaffected.
2026-05-15 11:22:15 -04:00
Barry DeenandGitHub 296b5f9702 Merge pull request #531 from barrydeen/refactor/remove-remote-signer
refactor(auth): remove NIP-55 remote signer support
2026-05-15 10:31:04 -04:00
Barry Deen 722bd3c326 refactor(auth): remove NIP-55 remote signer support
Wisp can now only sign events with a locally-stored nsec. Existing
REMOTE accounts are logged out on first launch via a one-shot
migration that filters them from the registry and scrubs the
indexed signer_package keys. Other accounts (LOCAL, READ_ONLY) are
preserved; the user is auto-switched to a remaining account or
dropped on the AuthScreen if none remain.

- Delete RemoteSigner, RemoteSignerBridge, SignerIntentBridge, and
  the two signer exceptions
- Drop the "Login with Signer" button and intent launcher from
  AuthScreen
- Collapse SigningMode to { LOCAL, READ_ONLY }
- Remove the REMOTE gift-wrap deferral branches in DmListViewModel
  and EventRouter (decryption is always local now)
- Drop the dead contentResolver ctor param on WalletViewModel
- Remove the nostrsigner: <queries> intent filter and 9 related
  strings across all locales
2026-05-15 10:29:52 -04:00
Barry DeenandGitHub 80e54a14e7 Merge pull request #530 from barrydeen/refactor/google-deterministic-accounts
refactor(auth): derive Google-linked nsecs deterministically (replaces Drive backup)
2026-05-14 12:08:59 -04:00
Barry Deen 3f3d277ef1 feat(onboarding): always include relay.wisp.talk in new user's NIP-65
When a new user finishes profile setup, OnboardingViewModel.finishProfile
saves the discovered+tested relays locally and publishes a kind 10002
relay list event. Insert wss://relay.wisp.talk into that list before
saving and publishing, so every new Wisp account advertises Wisp's relay
to the network from day one.

Read+write so the new user both publishes to and reads from it. Dedup
on URL (case-insensitive) so we don't duplicate if probing already
discovered the relay independently.

Reverts the earlier change to RelayConfig.DEFAULTS — that was the wrong
place. The DEFAULTS list is a fallback for users without an onboarding
flow; new-account injection belongs in onboarding where the relay list
event is actually constructed and published.
2026-05-14 12:05:05 -04:00
Barry Deen 86b7b885bb feat(relays): add relay.wisp.talk to default read+write relays
Adds Wisp's own relay to RelayConfig.DEFAULTS so every new account
picks it up automatically. Covers all signup paths via the existing
loadRelays() fallback — Sign Up, Google auto-create-at-0, Google
"Create another account", and read-only npub login.

Existing users with an explicitly-saved relay list are unaffected
(their stored list takes precedence). Existing users who never saved
a custom relay list will pick up wisp.talk on next load.

Read+write so new accounts both publish to and read from it.
2026-05-14 12:02:05 -04:00