Commit Graph

13865 Commits

Author SHA1 Message Date
Vitor Pamplona d42482ff56 Merge pull request #2990 from vitorpamplona/claude/add-i2p-privacy-option-nK2X7
Add I2P support with unified privacy routing
2026-05-19 16:56:54 -04:00
Claude 40995b9670 refactor(privacy): drop I2pType.INTERNAL — EXTERNAL is the permanent answer
Earlier commits kept I2pType.INTERNAL as a placeholder for a follow-up
embedded daemon. We're not shipping that: I2P bootstrap on Android is
structurally minutes-long (no equivalent of Tor's hardcoded directory
authorities — NetDB peer discovery is protocol-inherent), so an embedded
router would mean a permanently-warming UX. Users who want I2P run i2pd /
Java I2P independently and point Amethyst at its SOCKS port.

Changes:
- commons I2pType drops the INTERNAL variant; parseI2pType collapses unknown
  codes to OFF
- I2pManager loses its INTERNAL switch arm
- I2pSettingsDialog drops the "treat INTERNAL as OFF" mapping it used to
  carry — the enum no longer has the case
- Android UI I2pSettings.resourceId drops the i2p_internal branch
- strings.xml drops the now-unused i2p_internal string
- I2pSharedPreferences hardens load: a stored "INTERNAL" from an earlier
  branch is no longer a valid I2pType, so runCatching swallows the
  IllegalArgumentException and the user lands on OFF
- PrivacyRouterTest fixtures use I2pType.EXTERNAL throughout
2026-05-19 20:37:53 +00:00
Vitor Pamplona a8b6766f49 Merge pull request #2989 from vitorpamplona/claude/fix-zoomable-dialog-animation-3U3DK
Fix zoomed image dismiss animation to start from actual bounds
2026-05-19 15:59:18 -04:00
Claude 76c4771b58 fix(ui): start zoomable dialog close animation from zoomed bounds
The close animation lerped from the image's unzoomed layout bounds to
the source thumbnail rect, so dismissing a zoomed-in image jumped: the
image was visible at the inner zoomable's transformed bounds but the
exit animation rewound from the layout bounds instead.

Hoist the per-page ZoomState up to the dialog and feed its live scale +
offset into the outer graphicsLayer, so the grow/shrink animation
matches whatever the user is currently seeing on screen. At identity
zoom (entry case) behavior is unchanged.
2026-05-19 19:42:07 +00:00
Vitor Pamplona 1f0eab5f1d Merge pull request #2985 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 15:36:57 -04:00
Vitor Pamplona bcb6f131dd Merge pull request #2988 from vitorpamplona/claude/review-bitcoin-blockchain-plan-6NGZ5
Plan: local Bitcoin headers explorer for NIP-03 OTS (revised 2026-05-19)
2026-05-19 15:36:42 -04:00
Claude 24cf3fdc5f docs: update onchain-zap + headers-explorer plans for NIP-BC inline SPV tags
nostr-protocol/nips#2332 adds optional ["block", …] and ["proof", …] tags
on kind:8333 that ship the SPV proof inline. The data-model side is already
shipped in Quartz (BlockTag, ProofTag, OnchainZapEvent.block()/.proof() and
the TagArray helpers), but no production code path produces or consumes
either tag yet.

amethyst/plans/2026-05-14-onchain-zaps.md — onchain zaps plan:
- Update the "Chain backend" decision bullet to flag the spec change and
  the two production gaps (send-side emit, receive-side consume).
- Add a dedicated "Inline SPV proofs" section covering: spec status and
  the merkle-proof encoding ambiguity flagged back to the PR; per-layer
  status matrix (shipped vs gap, with file locations); send-side
  two-publish design (Design B — keep instant receipt, add post-confirm
  republish with block+proof; dedupe by (txid, target)); receive-side
  fast-path with fall-through on proof failure (never hard-reject);
  phased delivery G.1–G.7 with effort estimates (~3–4 d after S1 ships
  and spec encoding lands).
- Add the two new pending items to the existing "What's still pending"
  list to keep that section authoritative.

quartz/plans/2026-05-08-local-headers-explorer.md — headers-explorer plan:
- Rewrite §19 (follow-up onchain-zap verification) to reflect the spec
  change. Original section assumed we'd need BIP-37 merkleblock or
  full-block fetch over P2P; with the proof inline none of that
  infrastructure is required. Estimate collapses from 10–15 d to ~3–4 d.
- Point §19 at the full implementation plan in the onchain-zaps file,
  keeping S1 focused on OTS while the follow-up details live with the
  rest of the NIP-BC work.
2026-05-19 19:17:36 +00:00
Vitor Pamplona 3f458f8316 Merge pull request #2987 from vitorpamplona/claude/sync-notification-filters-RbvPY
Align push notifications with in-app feed filtering
2026-05-19 14:55:27 -04:00
Claude d19bcf07b0 docs(quartz): add interop-test-vectors section to local-headers-explorer plan
Every consensus-relevant layer of the planned headers explorer (BlockHeader80
parser, DifficultyTarget compact↔target, CalculateNextWorkRequired retarget,
MedianTimePast, header validator end-to-end, P2P wire codecs, reorg/chain
selection, OTS proofs) is pinned to upstream test vectors committed under
quartz/src/commonTest/resources/bitcoin/, matching the existing
nip44.vectors.json / bip39.vectors.json / mls/*.json pattern.

The single highest-value test is a nightly differential check asserting
LocalHeadersBitcoinExplorer.blockHash(h) == OkHttpBitcoinExplorer.blockHash(h)
for every height in [checkpoint, tip] — any consensus drift surfaces as a
disagreeing height.

Maps cleanly onto the existing Phase 1/3/4/5/7/9 work, adding ~5–7
engineer-days total to the plan budget. No new top-level phase needed.
2026-05-19 18:35:30 +00:00
Claude c75c2013fb fix(notifications): resolve addressable events to their replaceable note
LocalCache.getNoteIfExists(event.id) returns the id-keyed version note,
which has its replyTo moved to the replaceable note during insertion
(consumeBaseReplaceable). The id-keyed lookup therefore returns an empty
replyTo for AddressableEvent kinds — LongTextNoteEvent, WikiNoteEvent,
LiveChess*, VideoHorizontal/Vertical — and NotificationFeedFilter's
replyTo-based check in tagsAnEventByUser silently fails for replies into
long-form articles or wiki notes.

Switch to LocalCache.getNoteIfExists(event), which dispatches on
AddressableEvent and returns the address-keyed note with proper replyTo.
Applied in three places: the dispatcher predicate, consumeFromCache's
per-event match, and dispatchForAccount's muted-thread check (the last
one only handles non-addressable kinds today but is updated for
consistency).
2026-05-19 18:32:49 +00:00
Claude f3637dd7c1 docs(quartz): revise local-headers-explorer plan — SQLite, no bundle, OTS-only
Lock the design choices from the 2026-05-19 review against the intervening
2026-05-14 onchain-zaps work:

- Move the module from quartz/.../nip03Timestamp/bitcoin/ to a sibling
  quartz/.../bitcoin/ package so the headers explorer, header validator,
  peer pool and store can be reused by the future onchain-zap merkle-proof
  work without an inverted import path.
- Use androidx.sqlite + BundledSQLiteDriver for HeaderStore, matching the
  existing SQLiteEventStore. Schema lives in commonMain via IModule. Drops
  the hand-rolled flat-file + sidecar height index.
- Drop the bundled headers blob. Ship a single hardcoded PinnedCheckpoint
  constant; first-run sync starts from the checkpoint and pulls forward
  over P2P. APK growth: 0 bytes.
- Pre-checkpoint OTS heights fall through to OkHttpBitcoinExplorer via
  BitcoinExplorerEndpoint (shared with the onchain-zap EsploraBackend).
  Strict-mode users get an explicit error instead of a network call.
- Mark trustless NIP-BC onchain-zap verification as out of scope and
  capture it as a follow-up plan (BIP-37 merkleblock or full-block fetch
  on top of this stack).

Resolves open questions Q1, Q2 and Q4 from the original plan; Q3 (Quartz
public API vs internal) left open for Phase 0.
2026-05-19 18:14:30 +00:00
Claude 36e285f684 fix(notifications): WakeUp bypass + lookup hoist + comment cleanup
- WakeUpEvent.notifies is unconditionally true (wake every signed-in
  account). The previous commit's isTaggedUser-only routing dropped
  WakeUps that didn't happen to p-tag a logged-in pubkey, breaking the
  wake-the-device-for-everyone semantic. Both the dispatcher predicate
  and consumeFromCache now bypass the feed-style match for WakeUpEvent.

- LocalCache.getNoteIfExists(event.id) is hoisted out of the per-pubkey
  any-loop in the dispatcher predicate and out of the per-account forEach
  in consumeFromCache. The note is the same for every account, so one
  lookup per event is enough.

- Drop RepostEvent / GenericRepostEvent from the new muted-thread check
  in dispatchForAccount: they aren't routed below to any notify(), so the
  guard was partial dead code. Comment updated to call out that push has
  no repost notifier today (the feed does — separate gap).

- Comment on the dispatcher predicate now flags the two behavior deltas
  vs. the old event.notifies routing: WakeUp bypass, and NIP-22 Comments
  where the user is only the root author (uppercase `P`) no longer push.
2026-05-19 18:12:44 +00:00
Vitor Pamplona 8d4089e7e9 Merge pull request #2986 from vitorpamplona/claude/fix-illegal-argument-exception-V4AAM
Fix feed sorting race condition with snapshot-based comparator
2026-05-19 14:05:33 -04:00
Claude df87557ef3 fix: stable sort in ShortsFeedFilter to avoid TimSort contract crash
A newer AddressableEvent (e.g. VideoVerticalEvent) arriving on a relay
thread can swap a Note's event mid-sort, changing the createdAt value
the comparator returned moments earlier. TimSort then trips
"Comparison method violates its general contract!" and the feed
refresh crashes.

Snapshot createdAt once per note before sorting via a new
sortedByDefaultFeedOrder() helper.
2026-05-19 17:57:15 +00:00
Claude 49b244f026 fix: align push notifications with Notifications feed filter
Push notification routing now uses the same recipient + per-kind logic the
in-app Notifications feed uses (isTaggedUser + tagsAnEventByUser), so the
two surfaces agree on what counts as a mention, reply, citation, fork, or
community moderation. Adds the explicit muted-thread check for
reactions/zaps/reposts that the feed performs on the target note.

tagsAnEventByUser is hoisted to NotificationFeedFilter's companion so the
dispatcher and consumer can call it without depending on a feed instance.
2026-05-19 17:26:33 +00:00
Crowdin Bot 8b3ed48fb6 New Crowdin translations by GitHub Action 2026-05-19 17:21:37 +00:00
Vitor Pamplona ed686b71ed Merge pull request #2981 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 13:20:01 -04:00
Vitor Pamplona 37ea5dea01 Merge pull request #2984 from vitorpamplona/claude/fix-zap-validation-6CbFV
NIP-57 Appendix F zap receipt validation with LNURL provider verification
2026-05-19 13:19:52 -04:00
Crowdin Bot 100533dfd7 New Crowdin translations by GitHub Action 2026-05-19 17:14:23 +00:00
Vitor Pamplona 88fd15eefc Merge pull request #2982 from vitorpamplona/claude/fix-video-notification-clickable-O811N
Fix playback notification tap target in warm-pool fast path
2026-05-19 13:12:37 -04:00
Vitor Pamplona 872533f102 Merge pull request #2983 from vitorpamplona/claude/fix-empty-transaction-chips-edyYE
Improve onchain transaction filter UX with separate empty states
2026-05-19 13:12:13 -04:00
Claude 0c2eecf62c fix: make playback notification tap open the note on warm-pool resume
The playback notification's tap target (MediaSession.setSessionActivity)
was only bound from MediaSessionCallback.onAddMediaItems, which fires
when the in-app MediaController calls setMediaItem. GetVideoController's
warm-pool fast path skips setMediaItem when the acquired ExoPlayer was
retained with the same mediaId, so the new MediaSession was left with
no session activity and tapping the notification did nothing.

Bind the PendingIntent in newSession() from the acquired player's
current MediaItem extras, and share the construction with onAddMediaItems
via a single helper.
2026-05-19 16:33:55 +00:00
Claude 4346427e9f Validate zap receipts against LNURL provider's nostrPubkey (NIP-57 Appendix F)
Receipts were only being checked for a valid event signature — anyone could
sign a kind:9735 and have it counted toward another user's zap totals. NIP-57
Appendix F mandates three additional checks: receipt.pubkey == LNURL
provider's nostrPubkey (MUST), bolt11 invoice amount == zap request "amount"
tag (MUST), and lnurl tag == recipient's lnurl (SHOULD).

- Adds LnZapReceiptValidator + LnurlForm in quartz commonMain (pure logic).
- Adds LnurlEndpointCache (jvmAndroid) and the LnurlEndpointResolver
  interface for async lookup. The cache is primed by outbound zaps (existing
  LightningAddressResolver fetches now extract nostrPubkey) and on demand for
  inbound receipts when no entry is present.
- Adds OkHttpLnurlEndpointResolver in commons, wired into LocalCache via
  AppModules using the existing money-tier OkHttp builder (so Tor settings
  apply).
- LocalCache.consume(LnZapEvent) now: drops receipts that fail MUST checks
  synchronously when the cache is warm, defers credit until async resolution
  finishes on cache miss, and falls back to legacy signature-only behavior
  when no resolver is wired (tests).
- LnZapRequestEvent.create() now accepts amountMillisats + lnurl; both are
  threaded through Account.createZapRequestFor and emitted as tags so future
  receipts can be validated against them.

21 new tests cover validator reasons, lnurl form canonicalization across
lud16/URL/bech32, and cache eviction.
2026-05-19 16:19:29 +00:00
Claude cb6e1fe696 fix: keep filter chips visible on empty onchain transactions list
When a filter (e.g. Non-Zaps) excluded every loaded transaction, the
empty-state composable replaced the whole screen body — including the
filter chip row — so the user couldn't switch back to All or Zaps
without leaving the screen. Expose `hasAnyTransactions` from the
ViewModel so the screen distinguishes "no chain rows at all" from "no
rows for this filter": the chips stay rendered as long as any
transaction has loaded, and the LazyColumn shows a per-filter empty
message inline beneath them. Also drop the bc1 address header from the
list since the screen title already identifies the wallet.
2026-05-19 16:18:33 +00:00
Vitor Pamplona 1afa86cb26 Merge pull request #2974 from vitorpamplona/claude/clickable-wallet-card-hHTbM
Add on-chain transaction history screen with pagination
2026-05-19 10:35:31 -04:00
Vitor Pamplona facdd3dc8f Merge pull request #2978 from davotoula/feat/hashtag-limit-plurals-i18n
Convert hashtag-limit message to <plurals>
2026-05-19 10:35:05 -04:00
Vitor Pamplona 439331722a Merge pull request #2973 from vitorpamplona/claude/stop-video-background-timeout-jSLCO
Release MediaController after 30s background timeout
2026-05-19 10:34:44 -04:00
Vitor Pamplona 7b720a9a8d Merge pull request #2977 from davotoula/feat/onchain-zaps-reactions-gallery
Show on-chain Bitcoin zappers as a dedicated ₿ row in the expanded reactions gallery
2026-05-19 10:34:19 -04:00
Vitor Pamplona e94e1782c1 Merge pull request #2975 from nrobi144/fix/desktop-deb-launch-crash
test(desktop): add Compose UI smoke test + release .deb launch CI
2026-05-19 10:33:49 -04:00
Vitor Pamplona a747e20c86 Merge pull request #2980 from vitorpamplona/claude/debug-longpress-root-note-4UR5I
Fix popup menu positioning and parameter naming
2026-05-19 10:01:12 -04:00
Claude f2f02ab5cb fix(thread): restore long-press on root note + anchor popup to the card
Two related fixes to the thread quick-action popup:

- ThreadFeedView.FullBleedNoteCompose declared a `modifier` parameter
  that NoteMaster used to attach `combinedClickable(onLongClick = showPopup)`,
  but the body built a fresh `Modifier.fillMaxWidth().padding(top = 10.dp)`
  for its root Column and discarded the incoming modifier. Long-press on
  the root note in thread view never fired. Spread the incoming modifier
  onto the Column.

- LongPressToQuickAction emitted the Popup as a sibling of the content
  with no wrapping layout. The Popup's `parentLayoutCoordinates` then
  resolved to the enclosing LazyColumn, so `alignment = Alignment.Center`
  centered the menu on the whole list (visually middle of the screen)
  instead of the long-pressed card. Wrap content + Popup in a Box so the
  popup's parent bounds match the note card.
2026-05-19 13:45:56 +00:00
David Kaspar 61a282ba28 Merge branch 'main' into feat/hashtag-limit-plurals-i18n 2026-05-19 15:39:24 +02:00
David Kaspar 5973429860 Merge pull request #2979 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 15:38:46 +02:00
Crowdin Bot e1e185a6f2 New Crowdin translations by GitHub Action 2026-05-19 13:26:50 +00:00
Vitor Pamplona de222d152b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-19 09:24:34 -04:00
davotoula 8c3868bc32 i18n: convert hashtag-limit message to <plurals> and add cs/pt-BR/sv/de translations 2026-05-19 14:36:17 +02:00
davotoula cf44c092dd Code review:
- invalidate zaps flow when removeAllChildNotes clears onchainZaps
- simplify on-chain zap gallery after review
2026-05-19 13:10:02 +02:00
davotoula 91ded74636 Show on-chain zappers in expanded reactions gallery
- Add OnchainZappedIcon and PendingClockBadge
- move PendingClockBadge to TopStart to avoid follow-dot clash
2026-05-19 13:10:02 +02:00
David Kaspar d41cf6d75b Merge pull request #2976 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 12:34:53 +02:00
nrobi144 5ad21b6acd fix(desktop): disable ProGuard optimization entirely — fixes kmp-tor crash
Disabling individual sub-passes (method/specialization/returntype,
method/marking/static) was not sufficient. The interaction between
multiple optimization passes causes IncompatibleClassChangeError in
kmp-tor's AsyncFs.of() at launch. Shrink (dead code removal) stays
ON for the size win; only optimize is disabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 10:11:11 +03:00
Crowdin Bot 7c3399438d New Crowdin translations by GitHub Action 2026-05-19 06:50:42 +00:00
David Kaspar 733e2945bd Merge pull request #2972 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 08:49:16 +02:00
nrobi144 487dd3f2ac fix(desktop): disable method/marking/static ProGuard optimization
The smoke test discovered that ProGuard's method/marking/static pass
converts kmp-tor's AsyncFs.of() from instance to static. The JVM
then throws IncompatibleClassChangeError at launch because callers
still use invokevirtual.

Also updates smoke-test-desktop.yml trigger: runs on any PR touching
desktopApp/ (not just build config files).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:48:01 +03:00
nrobi144 c0c055e771 fix(desktop): restore java.management module — confirms #2819 fix
Reverts the intentional breakage from d0a6bbc96. The smoke test
proved it catches the crash: removing java.management from jlink
modules causes a fatal IncompatibleClassChangeError in kmp-tor
at launch (the module is needed for ManagementFactory + tor runtime).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:26:26 +03:00
nrobi144 d0a6bbc96f test(ci): intentionally remove java.management to verify smoke test catches #2819
This commit should FAIL the release-deb-launch CI job, proving the
smoke test works. Will be reverted in the next commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:05:10 +03:00
nrobi144 2010e41b25 fix(ci): allow dpkg post-install error, verify binary extracted
dpkg -i exits non-zero when the post-install script fails even with
--force-all. Allow the error with || true, then verify the binary
was actually extracted to /opt before proceeding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 07:59:33 +03:00
nrobi144 cca9c2ff3a fix(ci): use dpkg --force-all to skip xdg-desktop-menu error on runner
jpackage's post-install script calls xdg-desktop-menu which fails on
GitHub Actions runners with "No writable system menu directory found".
Menu registration is irrelevant for smoke testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 07:39:34 +03:00
nrobi144 cf6541a1e1 test(desktop): add Compose UI smoke test + release .deb launch CI
Fixes #2819 — v1.08.0 .deb crashed on Ubuntu with ManagementFactory
error because the build had no jlink modules() declaration. Current
main already has the fix; this PR adds CI to prevent regressions.

- Add compose.desktop.uiTestJUnit4 dependency
- DesktopLaunchSmokeTest: renders LoginScreen, asserts title text
- build.yml: xvfb for Linux leg so UI test runs on every PR
- smoke-test-desktop.yml: builds release .deb, installs it, launches
  under xvfb, verifies process stays alive 10s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 07:09:16 +03:00
Claude 7dce3a420f feat(wallet): time-windowed relay sub + coalesce zap bursts
Two improvements to onchain history attribution:

1. Coalesce UI updates. The OnchainZapEvent observation in the
   ViewModel now passes through .sample(500), so a relay flooding
   a backlog of historical zaps in quick succession produces at
   most one map update per 500ms instead of one per event. The
   downstream combine + StateFlow conflation handles the rest.

2. Bound relay queries to the visible window. New
   OnchainZapsFilterAssembler (SingleSubEoseManager-based) wakes
   only while the screen is on top and asks the user's inbox/
   outbox relays for kind-8333 events with since = the oldest
   visible blockTime — incoming (p-tag = user) and outgoing
   (authors = user) — so we don't drag the whole NIP-BC history
   when the user only scrolled through last week. As the user
   pages back, oldestBlockTime drops, the assembler re-subscribes
   with a wider window.

OnchainTransactionsScreen now wires OnchainZapsFilterAssemblerSubscription
with the StateFlow-derived window, and the coordinator owns the
parent assembler so the lifecycle matches other screen subs.
2026-05-19 00:52:12 +00:00
Claude 020d5195b5 feat(wallet): observe onchain zaps reactively, no more per-row scan
OnchainTransactionsViewModel now subscribes to LocalCache via
observeEvents for kind-8333 zaps that either p-tag the user
(incoming) or are signed by the user (outgoing), merging both into
a txid -> OnchainZapEvent map. The FilterIndex inside LocalCache
narrows the fanout so we only wake on relevant events.

The list of chain rows and the zap map are now independent
StateFlows combined into the displayed views, so a zap event
arriving after the page has loaded immediately attributes the
existing row to its Nostr counterparty without a refresh.

Replaces the per-row LocalCache.notes scan from the previous commit.
2026-05-19 00:35:31 +00:00