Commit Graph
1235 Commits
Author SHA1 Message Date
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
The Daniel fdc1832bf0 fix: display npub instead of raw hex pubkey in all fallback locations
Adds String.toNpub() extension (bech32 via Nip19) and updates ProfileData.displayString
to use it. All screens and components that fell back to a truncated hex pubkey when no
display name or NIP-05 was available now show the bech32 npub form instead.
2026-05-18 21:59:42 -04:00
The Daniel ab529ea6b0 feat: add nsec paste guard — block accidental private key pasting app-wide
Intercepts all text field input to detect nsec1... patterns and block them
before they reach the field state. A red warning pill floats above all
content (including dialogs and the keyboard) using a non-touchable,
non-focusable Dialog window. The keyboard is force-closed on block via
InputMethodManager so the pill is always visible.

The login/import screen is the only exception — it sets nsecPasteAllowed=true
via DisposableEffect for the duration of its lifecycle.

Also changes the onboarding "Publishing…" button label to "Creating account…".
2026-05-18 21:59:42 -04:00
The Daniel 36c2d014f6 feat(login): QR scan + watch-only mode for npub/nprofile
- QR scan button on the Nostr login sheet (eye icon first, QR second)
  scans nsec/npub/nprofile and auto-logs in; scanner rendered at
  Activity level so CameraX binds to the correct lifecycle owner

- nprofile1 login support: decodes TLV, extracts pubkey, saves as
  READ_ONLY alongside existing npub and hex-pubkey paths

- Watch-only onboarding: READ_ONLY logins skip the profile-setup flow
  and go to a two-step WatchOnlyOnboardingScreen (info + relay-wait
  spinner) before landing on the feed

- LocalCanSign CompositionLocal propagates signing capability through
  the entire nav tree; consumed by:
    - PostCard: hides full ActionBar (reply/react/repost/zap)
    - FeedScreen: hides compose FAB; reply callback is no-op
    - WispBottomBar: filters out Messages and Wallet tabs
    - WispDrawerContent: hides Messages and Wallet rows
    - UserProfileScreen: hides edit-profile, follow, zap, DM buttons

- Sidebar account picker shows eye icon next to watch-only accounts

- Keys screen branches: READ_ONLY shows "No private key is stored on
  this device" instead of the reveal button; LOCAL shows QR and copy
  buttons for both npub and nsec (nsec QR requires biometric reveal
  first; user avatar overlaid on both QR codes when available)

- Logout dialog swaps private-key backup warning for
  "Sign back in with your npub anytime" on watch-only accounts
2026-05-18 19:01:33 -04:00
The Daniel 7d35322bc1 fix(wallet): restore status-bar inset on Home dashboard
Hiding the app-bar removed the only thing providing top-edge spacing,
so the brand logo + refresh + settings row collided with the system
status icons. Reintroduce a clean inset on the Home content's root
Column via `statusBarsPadding()`; sub-pages keep their own app-bar,
which already supplies the equivalent gap.
2026-05-18 08:54:38 -04:00
The Daniel d87f7a9a66 feat(auth): swap Nostr ostrich icon for new pixel logo
Replace the curved ostrich vector with the new pixel-style Nostr mark
(purple #A223E9 body, orange #FD962C accents) on a 240x270 viewport.
2026-05-18 08:41:59 -04:00
The Daniel d466fb6840 feat(wallet): hide app-bar on home, NWC node alias on dashboard, Wallet Info expandable
Three iOS-parity additions to the wallet dashboard + settings.

1. Hide the top "← Wallet" app-bar on the Home dashboard. The bottom
   nav is the entry point, the dashboard's own logo + refresh +
   settings row plays the role of a toolbar, and the iOS app doesn't
   carry a redundant title strip either. Sub-pages still render the
   app-bar so back-nav stays reachable.

2. Render the NWC node alias to the right of the NWC logo in the
   dashboard top bar. The alias is fetched once post-connect via
   NIP-47 `get_info` (new request type in `Nip47.kt`), cached on
   `NwcRepository.nodeAlias`. NWC drawable also painted in its
   brand palette (warm orange + purple accent) so the top-bar mark
   matches iOS instead of rendering as a tinted black silhouette.

3. New Wallet Info expandable card in settings for both modes.
   Collapsed shows just the brand mark (+ alias on NWC) with a
   chevron; tap expands the per-mode detail rows:
   • Spark — Wallet ID (identity pubkey, truncated, copy), Network,
     SDK version.
   • NWC — Service pubkey, Client pubkey, Relay, Encryption,
     Lightning address (if set), Supported methods (chips).
   Spark identity pubkey is captured from the existing `getInfo()`
   call so no extra SDK roundtrip is needed. NWC connection metadata
   is exposed via a new `NwcRepository.ConnectionInfo` data class,
   updated whenever the connection string is parsed or encryption
   is renegotiated.
2026-05-18 08:09:43 -04:00
The Daniel 4496b31114 feat(wallet): outlined credit-card variant for unselected nav + drawer
Add `ic_wallet_outlined.xml` as the inactive twin of `ic_wallet.xml`
— identical card / stripe / chip geometry, the body switched from
a filled silhouette to a 1.5dp stroke. Stripe and chip stay solid
so the glyph still reads as a card-with-magstripe-and-chip when
hollow.

Where it's used:

  • Bottom nav wallet tab — solid icon when the tab is selected,
    outlined when not. Mirrors how the other tabs swap Material
    Filled/Outlined variants on selection.
  • Sidebar drawer wallet row — always outlined to match iOS,
    which renders the drawer wallet glyph hollow regardless of
    nav state.
  • Logout-warning embedded-wallet bullet — switched to outlined
    too so the icon vocabulary stays consistent inside the drawer.

Bottom-bar enum gains a second drawable slot so the
`selectedIconRes` / `unselectedIconRes` pair drives the swap
without per-tab branching in the composable.
2026-05-18 07:26:35 -04:00
The Daniel ae400abc23 fix(wallet): NWC brand colors + bottom-anchored mode picker rows
Two issues in the new two-tier connect screen:

  1. NWC logo rendered in the mode picker as a flat monochrome
     silhouette. The drawable was all #FF000000 and Compose had no
     explicit tint, so it fell through as black on the dark theme.
     Paint the two paths with their brand colors (warm orange + a
     purple accent on the upper sliver). The desaturated footer /
     settings renders continue to apply a colorFilter so the
     brand colors only show where the logo is the focal element.

  2. The mode picker's Spark + NWC rows rode up near the top of the
     screen instead of anchoring near the bottom. Root cause: the
     wallet routing wrapped every page in a verticalScroll Column,
     which gives the inner content unbounded height — so the
     fillMaxSize + weighted Spacer layout I built for the mode
     picker collapsed and the rows pinned to the copy stack.

     Fix: route the mode picker case through a separate non-
     scrolling Column. The other Spark setup screens (NwcSetup,
     SparkSetup, SparkRestoreSeed, SparkBackup) keep their
     verticalScroll wrapper because they have text inputs and
     status output that can grow.
2026-05-17 23:34:29 -04:00
The Daniel def48dc854 feat(wallet): dashboard parity with iOS — top bar, welcome banner, pill, inline transactions
Reshape the wallet home screen to match the iOS layout per
WALLET_PARITY.md §3.

Changes:

  • Top bar — Spark+Breez (or NWC) brand logo on the left, refresh
    + settings icons on the right. The wallet branding moves up from
    the bottom "powered by" footer; the standalone refresh button
    that used to live under the balance number is gone.
  • Welcome banner — default (nsec-derived) wallets get a tappable
    card with a key icon, title "Your default wallet is secured by
    your key", and body explaining the nsec-as-backup property. Tap
    routes to the recovery-phrase screen so users can also save the
    seed locally. Hides once seedBackupAcked flips. Custom wallets
    keep the existing amber seed-not-viewed nudge and red backup-
    missing warning.
  • Lightning address pill — collapsed surface chip with bolt icon
    prefix, tap-to-copy. Replaces the bare primary-color text line.
    Setup CTA (when no address is registered) renders as the same
    pill shape with an outlined accent border.
  • Send / Receive — circular 64dp accent-tinted buttons in a single
    centered row (was 72dp on primary container). Extracted as
    WalletActionButton.
  • Recent transactions footer — anchored to the bottom of the
    dashboard. "RECENT" header + "View all ^" affordance with the
    most-recent transaction visible inline. Tap any of it to expand
    to the full transactions screen.

The dashboard preloads recent transactions on connect via a
LaunchedEffect so the inline footer has data without the user
having to visit the transactions screen first.

iOS counterpart: this layout already ships on iOS; brings Android
visually in line.
2026-05-17 23:26:40 -04:00
The Daniel e2d64c9b32 feat(wallet): universal credit-card wallet icon
Replace the Material AccountBalanceWallet glyph with a credit-card
silhouette vector drawable per WALLET_PARITY.md §3.1. Used uniformly
in:

  • Bottom nav wallet tab
  • Sidebar drawer wallet row
  • Logout-warning embedded-wallet bullet

The drawer wallet row previously switched between three icons based
on the user's zap-icon preference / fiat-mode setting (CurrencyBitcoin
/ ic_bolt / AccountBalanceWallet). That conditional is removed — the
wallet icon is fixed per spec because wallet ≠ zap glyph; the two
settings control different visuals.

The new vector uses `?attr/colorControlNormal` for fill so it tints
correctly in both selected/unselected nav states and works across
themes without per-call colorFilter rewiring.
2026-05-17 23:17:05 -04:00
The Daniel 7c94dfd250 feat(wallet): two-tier Connect screen + Spark sub-screen
Restructure the wallet connect UX per WALLET_PARITY.md §2.6 to match
the iOS flow. Top-level screen is a minimal mode picker (Spark wallet
/ Nostr Wallet Connect) with a centered logo + intro copy; tapping
Spark navigates to a sub-screen with four explicit options:

  • Use my default wallet (gated on hasKeypair)
  • Create new wallet
  • Restore from seed phrase
  • Restore from relays

This fixes the long-standing discoverability gap where the
nsec-derived-wallet recovery path was only reachable via a button
labeled "Create a New Wallet" — actively misleading users trying to
recover an existing wallet. The "Use my default wallet" CTA is now a
first-class row in the Spark sub-screen.

Two behavioral changes required to make the new flow reachable:

  • Remove the silent maybeAutoCreateDefaultWallet() trigger from
    navigateHome(). Before, returning users with an nsec but no
    local wallet were auto-derived behind the scenes and never saw
    the mode picker. Now they see it and opt in via "Use my default
    wallet" — matches iOS §2.2. Fresh signups still auto-derive via
    the existing onboarding path (OnboardingViewModel.generate-
    DefaultFromPrivkey), so account-create is unchanged.
  • Remove the auto-fire of autoCheckRelayBackup() from
    selectSparkMode(). Per §2.6 Screen 2 the sub-screen has no
    surprise UI — users explicitly tap "Restore from relays" to
    search.

Seed-entry moves to its own WalletPage.SparkRestoreSeed sub-screen
with a back arrow, replacing the inline OutlinedTextField that was
mixed into the old SparkSetupContent.

Strings updated to match the §2.6 copy reference verbatim. Orphan
strings (wallet_connect_wallet, wallet_choose_how, etc.) removed.
2026-05-17 22:50:22 -04:00
The Daniel 48a27558cc test(wallet): lock nsec → Spark derivation contract with cross-platform vectors
Pin the HKDF + BIP39 outputs for the deterministic Spark wallet
derivation so cross-platform regressions are caught at unit-test time.
Two fixed privkeys (`01 × 32` and `02 × 32`) feed through the same
HKDF-SHA256(salt="wisp-spark-wallet-v1", info="entropy", L=16) +
BIP39-English path that production uses; the resulting entropy +
mnemonic are asserted byte-for-byte.

HKDF + entropyToMnemonic are inlined in the test rather than imported
from `Keys` / `SparkRepository` because `Keys` triggers secp256k1-kmp
JNI init at class load, which isn't available in the JVM unit-test
classpath. The inlined helpers mirror the production code 1:1 — if it
drifts, the vectors stop matching and we catch it.

Also expand WALLET_PARITY.md §1.3 with the generated values and a
pointer to the test, and update §11.1 / §12.1 to reflect that the
vectors are no longer blocked on Android. iOS can now add an XCTest
that asserts the same expectations to close out §11.1.
2026-05-17 22:18:24 -04:00
The Daniel 8dd897ccca refactor(compose): inline placeholder instead of floating label
The compose text field used a Material-3 floating label that sat
above the body and pulled the eye away from what the user was
typing. Swap it for an inline placeholder that lives inside the
field and disappears on first keystroke — matches the iOS hint
style and reduces visual chrome around the editor.

Same swap for the gallery-mode caption field.
2026-05-17 20:29:39 -04:00
The Daniel 3d543948bc feat(compose): scroll preview into view when undo countdown starts
For a long body or a body with attachments, the post-preview card
sits below the fold while the user is typing. When the user taps
Publish, the countdown starts but the preview they're meant to
spot-check is still off-screen.

Hoist the scroll state, anchor a zero-height Spacer just before the
preview card (kept in the layout regardless of visibility so its
position is always valid), and animate-scroll to that anchor once
the keyboard has actually dismissed during a countdown. Gated on a
non-empty body so it's a no-op when there's nothing to spot-check.

Mirrors barrydeen/wisp-ios#134.
2026-05-17 20:29:18 -04:00
The Daniel 02565bcd6c feat(compose): countdown progress bar + compact cancel button
Replace the symmetric Undo / Post Now pill pair with a single
progress-bar pill that fills left-to-right over the undo window.
Tapping the bar publishes immediately. Cancel collapses to a small
red circle with an X glyph so the affirmative action gets the visual
weight and the destructive one no longer competes for attention.

Progress is driven by a ~60fps LaunchedEffect reading a snapshot
countdownStartedAt timestamp on the view model — smooth motion
instead of one-second-per-step. Fill is a rectangle masked to the
outer capsule shape so both ends stay rounded regardless of how
full the bar is.

Also lock the Publish button to 44dp with zero content padding so
both states share identical bottom-bar geometry, and switch the
bottom bar wrapper to symmetric vertical padding.

Mirrors barrydeen/wisp-ios#134.
2026-05-17 20:28:24 -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