Commit Graph
100 Commits
Author SHA1 Message Date
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 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
Barry Deen de88bdd89a fix(auth): skip chooser screen when no accounts found
Previously a first-time user with no Nostr activity yet would land on a
chooser screen with an empty list and a "Create your account" prompt
explaining the deterministic-derivation model. Cuts an extra tap and
some explanatory copy.

Now: if the probe returns zero accounts, auto-derive the keypair at
index 0 and transition directly to ONBOARDING_PROFILE. The chooser
screen only appears for users who actually have existing accounts to
pick from.

Deterministic derivation means auto-creating at index 0 is safe: if a
later sign-in (e.g. after a flaky probe) discovers existing activity
at index 0, the same nsec is regenerated, so no identity is lost.
2026-05-14 11:45:46 -04:00
Barry Deen 3d3c6e12ef refactor(auth): derive google-linked nsecs deterministically
Replaces the Google Drive backup flow (#528) with deterministic key
derivation. The user's Nostr identity IS their Google account — no
encrypted blobs to store, no backup events to publish, nothing for
Google or any third party to retain.

   privkey = SHA-256("wisp-account-v1:" || sub || ":" || accountIndex)

Properties:
- Same Google account always derives the same nsec on any device
- No backup to lose: signing in regenerates the keys
- No `drive.appdata` OAuth scope, no scary Drive consent dialog
- Anyone with access to the Google account can derive every nsec.
  Bounded by Google account security — same trade-off as #528, with
  a much simpler attack surface and no third-party storage layer

Discovery on sign-in:
- Derive candidate keypairs for indices 0..15 from the user's `sub`
- One REQ to relay.damus.io, relay.primal.net, nos.lol, nostr.wine,
  relay.wisp.talk, relay.ditto.pub asking for kind 0/3/10002 events
  from those pubkeys
- Pubkeys with any activity = "in use" accounts that go in the chooser;
  avatar + display name come from the same kind-0 events
- "Create another account" derives the next-unused index

Code shrinkage: DriveBackupService is gone, BackupCrypto's encryption
helpers are gone, the play-services-auth dependency is gone, and the
Drive-related ProGuard rules are gone. The whole flow is ~200 fewer
lines than #528 and easier to audit — the derivation is one line of
SHA-256.

Splash button switches to Google's dark-mode brand variant (#131314
container, full-color G, #8E918F stroke) per Sign in with Google spec.

No migration needed: nobody is on the #528 flow yet.
2026-05-14 11:40:37 -04:00
Barry DeenandGitHub 403b25ac90 Merge pull request #528 from barrydeen/feat/google-drive-backup
feat(auth): Continue with Google + encrypted nsec backup to Drive
2026-05-13 22:06:41 -04:00
Barry Deen 270425aa77 feat(auth): continue with google with encrypted nsec backup to drive
Adds a third splash entry point alongside Sign Up / Log In:

- Credential Manager + Google Identity for sign-in; AuthorizationClient
  scopes drive.appdata so Wisp can read/write a hidden per-app folder
- Per-account backup files named wisp_nsec_<npub>.bin in appDataFolder.
  One Google account can back up many Nostr identities; sign-in lists
  every backup found and the chooser shows each with avatar + display
  name fetched from relay.damus.io / relay.primal.net (kind 0)
- Encryption key derived via HMAC-SHA256 from the Google sub claim,
  reusing Nip44's XChaCha20 + HMAC building blocks for the payload.
  Plaintext nsec never leaves the device
- "Create another account" is always available, even when prior backups
  exist, so users can keep adding identities without overwriting
- Google-branded splash button (white, full-color G logo) per Sign in
  with Google brand spec
2026-05-13 22:04:45 -04:00
Barry DeenandGitHub 517af030ba Merge pull request #527 from dmnyc/feat/media-layout-gallery
feat(media): horizontal swipe gallery for multi-image posts
2026-05-13 19:50:19 -04:00
Barry DeenandGitHub e5b379a08f Merge pull request #526 from barrydeen/feat/mention-relay-search-fallback
feat(mention): query search relay when local results are insufficient
2026-05-13 11:52:02 -04:00
Barry Deen 28bdd4a7c0 feat(mention): query search relay when local results are insufficient
When typing @mentions, first check local follows and cached profiles.
If fewer than 5 matches are found, query wss://search.nostrarchives.com
(NIP-50) and stream results into the suggestion list as they arrive.
The relay subscription is cancelled on each new keystroke and cleaned
up on EOSE or after a 3-second timeout.

Also adds @mention autocomplete to the livestream chat input, which
previously had no mention support.
2026-05-13 11:51:03 -04:00
Barry DeenandGitHub 0335e5cf2e Merge pull request #525 from barrydeen/fix/panning-speed
Fix panning speed slow down when zoomed in
2026-05-13 11:18:10 -04:00
Barry Deen 4c3ff50e49 Fix panning speed slow down when zoomed in 2026-05-13 11:17:35 -04:00
Barry DeenandGitHub e2ea4c728b Merge pull request #521 from dmnyc/feat/keys-remote-signer-details
feat(keys): show remote signer details on the Keys screen
2026-05-12 22:59:12 -04:00
Barry DeenandGitHub d8e29c5cf7 Merge pull request #523 from greenart7c3/claude/add-auto-translate-notes-1BCVd
Add auto-translate feature for notes
2026-05-12 22:58:52 -04:00
Barry DeenandGitHub 968e89d18d Merge pull request #522 from greenart7c3/claude/fix-relay-background-disconnect-wbnMC
Pause/resume local relay on app lifecycle events
2026-05-12 22:55:37 -04:00
Barry DeenandGitHub b4e361a82c Merge pull request #519 from dmnyc/feat/zap-fiat-input
feat: register-style fiat input on the zap dialog
2026-05-06 23:30:39 -04:00
Barry DeenandGitHub 9a6b100e3a Merge pull request #517 from barrydeen/chore/bump-version-1.0.5
chore: bump version to 1.0.5 (79)
2026-05-04 09:28:08 -04:00
Barry Deen d5a6f729d2 chore: bump version to 1.0.5 (79) 2026-05-04 09:26:26 -04:00
Barry DeenandGitHub 00d70f20f0 Merge pull request #516 from barrydeen/fix/bottom-tab-state-restore
fix: stop restoring stale tab back stacks on bottom-nav switch
2026-05-04 09:25:29 -04:00
Barry Deen 5b745ddb90 fix: stop restoring stale tab back stacks on bottom-nav switch
PR #514 changed the bottom-nav handler to use popUpTo with the graph's
start destination plus saveState/restoreState. Two regressions resulted:

- Tapping a tab restored the tab's saved back stack, returning the user
  to a previously-open thread instead of the tab's main screen.
- The graph's start destination (LOADING) is popped inclusive on first
  successful load, so popUpTo never matched. The resulting back-stack
  state could surface the splash/auth screen on system back from
  Notifications.

Revert that block to popUpTo(FEED) { inclusive = false } + launchSingleTop,
which always lands the user on the tab's main screen with a shallow stack.
The refreshDmsAndNotifications() throttle introduced alongside the broken
nav block is preserved — that part addresses real REQ churn jank and is
independent of how the back stack is structured.
2026-05-04 09:23:09 -04:00
Barry DeenandGitHub 04501e41de Merge pull request #515 from barrydeen/fix/tab-switch-jank
perf: reduce startup and feed rendering work
2026-05-04 08:33:37 -04:00
Barry Deen a55ebdfee6 chore: bump version to 1.0.4 (78) 2026-05-04 08:33:22 -04:00
Barry Deen c4a8aacee3 perf: restore tight NIP-05 timeouts, isolate download client, harden notif rebuild
- give getNip05Client a dedicated 5/10s client (was aliasing the 10/15s
  general client); slow .well-known/nostr.json endpoints no longer tie
  up verification threads
- add getDownloadClient (30/60s) for MediaDownloader; the shared media
  client (10/30s) is fine for ExoPlayer streaming but its read timeout
  is too tight for full-file downloads on flaky networks
- tighten notification rebuild coalesce window from 50ms to one frame
  (16ms) so single-arrival updates feel responsive
- add NotificationRepository.shutdown() and call it from
  FeedViewModel.onCleared() so rebuildScope doesn't outlive the VM
2026-05-04 08:18:03 -04:00
Barry Deen e3373889e0 perf: reduce startup and feed rendering work 2026-05-04 08:08:44 -04:00
Barry DeenandGitHub de7af45149 Merge pull request #514 from barrydeen/fix/tab-switch-jank
fix: reduce bottom-tab switching jank
2026-05-04 07:46:53 -04:00
Barry Deen a0ec123e0c fix: preserve bottom-tab state during tab switching
Restore top-level tab destinations instead of recreating them on each tap, and avoid immediately resubscribing inbox streams when users bounce between tabs. This reduces navigation jank and repeated hangs on slower devices.
2026-05-04 07:44:39 -04:00
Barry DeenandGitHub b028007a7c Merge pull request #513 from barrydeen/chore/bump-version-0.3
bump
2026-05-03 21:59:32 -04:00
Barry Deen 55e3adfc74 bump 2026-05-03 21:58:58 -04:00
Barry DeenandGitHub 550b6d1997 Merge pull request #512 from barrydeen/feat/persist-decrypted-dms
Persist decrypted DMs to skip per-boot signer round-trips
2026-05-03 21:57:12 -04:00
Barry Deen 4894ccee40 feat: persist decrypted DMs to skip per-boot signer round-trips
NIP-17 gift wraps are re-fetched on every cold start (the kind-1059
subscription has no since cursor because gift-wrap timestamps are
randomized up to 2 days in the past). Without a decrypted-DM cache,
remote-signer mode (Amber) re-runs two NIP-44 IPC decrypts per wrap on
every launch.

Adds an ObjectBox-backed cache keyed by ownerPubkey|giftWrapId. On
construction, DmRepository hydrates conversations, seenGiftWraps,
rumorIdIndex and the DM notification list from disk off the main thread.
addPendingGiftWrap now short-circuits on seenGiftWraps before queuing,
so relay-redelivered wraps never reach the signer; only newly arrived
wraps are decrypted. addMessage / addReaction / addZap re-persist the
parent message so reactions and zaps survive restarts. clear() and
purgeUser() propagate to the persistence layer.
2026-05-03 21:54:41 -04:00
Barry DeenandGitHub 81aeaa05f8 Merge pull request #511 from barrydeen/refactor/remove-tor
refactor: remove Tor functionality
2026-05-03 21:16:01 -04:00
Barry Deen 0223e1b9e7 refactor: remove Tor functionality
Drops the embedded kmp-tor client, all .onion relay support, the Tor
toggle UI on splash/auth/drawer screens, the SOCKS proxy plumbing in
HttpClientFactory, all Tor-related strings across 11 locales, the
TorManager singleton, and the kmp-tor dependencies and ProGuard rules.

Relay URL validation now accepts only wss:// (with hostname, no port,
non-localhost, non-IP). The cleartextTrafficPermitted network security
flag stays — still needed for local relays on RFC1918 / loopback.
2026-05-03 21:14:51 -04:00
Barry DeenandGitHub 94b9d7b78c Merge pull request #510 from barrydeen/fix/relay-connect-anr
fix: dispatch Relay.connect() off the calling thread to avoid ANR
2026-05-03 20:51:25 -04:00
Barry Deen 955787eaad fix: dispatch Relay.connect() off the calling thread
OkHttpClient.newWebSocket() can block on its shared TaskRunner lock
for several seconds under contention. UI callbacks that invoke
RelayPool.sendToRelayOrEphemeral were calling Relay.connect() inline
on the main thread, producing 5s+ ANRs. Dispatch connect() through a
small dedicated thread pool so callers never wait on WebSocket setup.
2026-05-03 20:47:03 -04:00
Barry DeenandGitHub 683e3072b2 Merge pull request #509 from barrydeen/feat/inline-video-loop
feat: loop inline mp4 videos in note cards
2026-05-03 20:36:19 -04:00
Barry Deen 08d1340e9e feat: loop inline mp4 videos in note cards
Inline video players in RichContent now use REPEAT_MODE_ONE so MP4s
auto-repeat when rendered inside a note card. Applied to both freshly
created and PiP-reclaimed players. Fullscreen and audio paths are
unchanged.
2026-05-03 20:35:48 -04:00
Barry DeenandGitHub b6105eb134 Merge pull request #494 from dmnyc/feat/fiat-mode-coin-stack-icon
feat: coin stack icon for zaps in fiat mode
2026-05-03 20:01:28 -04:00
Barry DeenandGitHub 8d17d12a45 Merge pull request #502 from dmnyc/feat/post-undo-timer-settings
feat: post undo timer settings
2026-05-03 20:01:11 -04:00
Barry DeenandGitHub 0347c8f0e4 Merge pull request #503 from dmnyc/fix/drawer-item-spacing
fix: reduce drawer menu item spacing
2026-05-03 20:00:55 -04:00
Barry DeenandGitHub f857466455 Merge pull request #504 from dmnyc/fix/switch-unchecked-colors
fix: improve Switch visibility in unchecked state
2026-05-03 20:00:40 -04:00
Barry DeenandGitHub 1a10f6b84f Merge pull request #495 from fiatjaf/debug-build
use a different id and name for the app when doing a debug build
2026-05-03 20:00:13 -04:00
Barry DeenandGitHub 786710efbf Merge pull request #505 from fiatjaf/fix-spacing
add a spacing between "replying to" and the name
2026-05-03 19:59:14 -04:00
Barry DeenandGitHub 30bcaca875 Merge pull request #506 from fiatjaf/thumbhash
Thumbhash
2026-05-03 19:58:54 -04:00
Barry DeenandGitHub 413a06915e Merge pull request #500 from barrydeen/feat/qr-scan-drawer
feat: add QR scan tab to drawer QR sheet
2026-04-26 09:27:09 -04:00
Barry Deen fddae10109 chore: bump version to 1.0.2 (76) 2026-04-26 09:26:10 -04:00
Barry Deen c8d6e0d05f feat: add QR scan tab to drawer QR sheet
Adds a Scan tab alongside Nostr/Lightning in the drawer's QR sheet that
opens the camera and decodes Nostr entities (npub, note, nprofile, nevent,
naddr) — with or without the nostr: prefix — and navigates directly to
the matching profile, thread, or article.

Extracts the camera scanner from WalletScreen into a reusable QrScanner
component, and centralizes NostrUriData→route mapping as toRoute() in
Navigation.kt so the deep-link handler and scanner share it.
2026-04-26 09:26:10 -04:00
Barry DeenandGitHub 0b0d30dec2 Merge pull request #493 from barrydeen/fix/16kb-page-size
fix: support 16 KB page size for Android 15+
2026-04-23 11:03:49 -04:00
Barry Deen f68b40f5a7 fix: support 16 KB page size for Android 15+
Disable legacy jniLibs packaging so native libraries are stored
uncompressed and page-aligned in the APK. Bump secp256k1-kmp to 0.19.0
and kmp-tor-resource to 408.16.4 since the prior versions shipped
ELF binaries with 4 KB LOAD alignment. All 13 .so files in the release
APK now report 2**14 alignment.
2026-04-23 11:03:17 -04:00
Barry DeenandGitHub 0d8c856c29 Merge pull request #492 from barrydeen/feat/inline-video-download
feat: add download button to inline video controls
2026-04-23 10:36:06 -04:00
Barry Deen 3ab1041fce feat: add download button to inline video controls 2026-04-23 10:35:38 -04:00
Barry DeenandGitHub b609fc56c0 Merge pull request #491 from barrydeen/fix/notifications-freeze-indexof
perf: avoid O(N^2) indexOf in notifications list
2026-04-23 10:30:19 -04:00
Barry Deen 503c46b256 perf: avoid O(N^2) indexOf in notifications list
Replace items(...) + notifications.indexOf(item) with itemsIndexed(...)
in NotificationsScreen. The indexOf call ran during composition for
every visible item, causing a linear scan per item and quadratic total
work as the list grew. Reported as a UI freeze when tapping the
notifications tab on slower devices (GrapheneOS).
2026-04-23 10:29:43 -04:00
Barry DeenandGitHub f4c716551b Merge pull request #490 from barrydeen/fix/quoted-note-emoji-picker
fix: use user's emoji picker for quoted/referenced notes
2026-04-23 10:18:43 -04:00
Barry Deen 69e49eb648 fix: use user's emoji picker for quoted/referenced notes
Quoted notes nested inside another note were using the default unicode
emoji fallback instead of the user's sorted-by-frequency picker with
their custom emojis. Plumb resolvedEmojis, unicodeEmojis, and the
onOpenEmojiLibrary callback through NoteActions so nested PostCard /
GalleryCard renders in RichContent.QuotedNote pick up the same emoji
state as their parent.
2026-04-23 10:03:41 -04:00
Barry DeenandGitHub 642b68e854 Merge pull request #489 from barrydeen/feat/drawer-avatar-opens-profile-edit
feat: open edit profile when tapping drawer avatar
2026-04-23 09:50:07 -04:00
Barry Deen 0d8e34fe02 feat: open edit profile when tapping drawer avatar 2026-04-23 09:49:44 -04:00
Barry DeenandGitHub 05e14914b4 Merge pull request #488 from barrydeen/fix/media-downloader-oom
fix: stream media downloads to avoid OOM on large files
2026-04-23 09:44:22 -04:00
Barry Deen 7a29f976df fix: stream media downloads to avoid OOM on large files 2026-04-23 09:43:34 -04:00
Barry DeenandGitHub 5853db286b Merge pull request #487 from barrydeen/feat/persistent-audio-player
feat: persistent audio mini-player docked above bottom nav
2026-04-22 22:26:04 -04:00
Barry Deen 06ec5592d3 feat: persistent audio mini-player docked above bottom nav
Replaces the basic inline audio widget with a global mini-player that persists
across feed scrolling and tab navigation. Playback continues with system
notification / lock-screen transport controls via MediaSessionService.

The dock has two states: a collapsed row showing the author's avatar and name
with rewind-15 / play-pause / forward-15; swipe up to expand for a scrub slider,
speed cycling, and close. Single ExoPlayer is owned by AudioPlayerController
and shared with the inline tap-to-play widget, which now reflects global
playback state.
2026-04-22 22:21:41 -04:00
Barry DeenandGitHub ebeebccbe9 Merge pull request #486 from barrydeen/fix/persist-settings-across-account-switch
fix: preserve local relay and emoji frequency across account switches
2026-04-22 21:56:22 -04:00
Barry Deen e245b2fbde fix: preserve local relay and emoji frequency across account switches
Two repos called clear() during resetForAccountSwitch() were doing
`prefs.edit().clear().apply()` on their SharedPreferences files while
those files were still pointing at the outgoing account:

- CustomEmojiRepository wipes its own per-pubkey file, erasing the
  "most used" emoji frequency map. No relay mirror, so the data was
  lost for good.
- BlossomRepository shares `wisp_prefs_{pubkey}` with KeyRepository, so
  its full-file wipe also took out local_relay, relays, dm_relays, etc.
  Most of those keys came back via replaceable-event refetches, but
  local_relay has no network source and stayed null.

Drop the disk wipe from both clear() methods. The subsequent
reload(newPubkey) already repoints prefs to the incoming account's file.
2026-04-22 21:54:40 -04:00
Barry DeenandGitHub b0455b1ea6 Merge pull request #485 from barrydeen/fix/compose-mention-ux
fix: stop mention insert from breaking compose cursor and keyboard
2026-04-22 21:04:50 -04:00
Barry Deen ad8c1c4d11 fix: stop mention insert from breaking compose cursor and keyboard
Mentions used to be stored in the text field as raw nostr:nprofile URIs
rendered as @Name via an OutputTransformation. The visual/underlying
offset mismatch made the cursor jump, the IME desync on rapid typing,
forced a trailing space after every mention (so '@name's' was impossible),
and turned a single backspace into a silent delete of the whole mention.

Store '@Name' directly in the text field and track mention ranges
out-of-band in the ViewModel; splice nostr:nprofile URIs back in at
publish time. Editing is now plain text — identity offset mapping, no
forced space, and breaking a mention drops only that mention, not the
surrounding text.
2026-04-22 21:04:13 -04:00
Barry DeenandGitHub f0f728d2fc Merge pull request #484 from barrydeen/fix/trending-reconnect-race
fix: trending feed not reconnecting after app resume
2026-04-22 20:51:24 -04:00
Barry Deen 6582c16d88 fix: trending feed not reconnecting after app resume
Remove the preConnectEphemeral optimization from the reconnect path. It
created a race where the ephemeral was mid-connect when subscribeTrendingFeed
fired its REQ, leaving the subscription stuck on Connecting. The direct
metric-switch path (no preConnect) worked fine, so match it.
2026-04-22 20:50:52 -04:00
Barry DeenandGitHub f9cc586804 Merge pull request #483 from barrydeen/fix/pull-self-deletions-on-startup
fix: pull own NIP-09 deletions on startup so deleted sets stay gone
2026-04-22 20:14:00 -04:00
Barry Deen 359e64385b fix: fetch and apply own deletion events on startup
Relays sometimes keep serving hashtag sets (and other addressable
events) after the user publishes a NIP-09 kind 5 deletion, so sets
reappeared on app reload — especially after a fresh install or on a
second device where the local DeletedEventsRepository was empty.

Self-data subscription now also requests our own kind 5 events, and
EventRouter routes them through EventRepository (marking deletion
coords/ids) and sweeps any matching interest sets out of
InterestRepository so a late-arriving deletion still wins.
2026-04-22 20:12:12 -04:00
Barry DeenandGitHub fbc18d385b Merge pull request #482 from barrydeen/feat/delete-note-confirmation
feat: confirm before deleting a note
2026-04-22 20:01:36 -04:00
Barry Deen 984ef213e0 feat: confirm before deleting a note from the 3-dot menu 2026-04-22 20:01:10 -04:00