Commit Graph

13651 Commits

Author SHA1 Message Date
Vitor Pamplona a04e7ddc84 Merge pull request #2917 from vitorpamplona/claude/modernize-git-issues-design-dzomX
Add Git repository detail screen with issues/patches tabs
2026-05-15 18:20:01 -04:00
Vitor Pamplona 0953862cfd Merge pull request #2918 from vitorpamplona/claude/fix-bottombar-reorder-icons-M57CG
Remove collectAsStateWithLifecycle from BottomBarSettingsContent
2026-05-15 18:19:42 -04:00
Claude 47e503a023 fix: ingest NIP-34 status, PR, repo-state and grasp-list events
LocalCache.consume's type switch only had branches for kinds 1617
(patch), 1621 (issue), 1622 (reply) and 30617 (repository). Every
other NIP-34 event kind fell through to the else branch and was
silently dropped with a "Event Not Supported" warning, even though
Quartz's EventFactory parses them all.

This affected:

- kinds 1630-1633 (status open/applied/closed/draft) — the status
  pills on the new Git Repository screen and on issue/patch cards
  could never populate, since the status events themselves never
  reached LocalCache.
- kind 1618 (pull request) — the Patches/PRs tab on the repository
  screen would never list PR events.
- kind 1619 (PR update) — couldn't be observed, even though Quartz
  models the parent-PR relationship.
- kind 30618 (repository state) — branch/tag pointers were dropped.
- kind 10317 (user grasp server list) — analog to NIP-65, dropped.

Wires each into the existing consumeRegularEvent /
consumeBaseReplaceable helpers so they land in the cache where the
screen filters, GitStatusIndex, and any future UI surfaces can see
them.
2026-05-15 22:02:31 +00:00
Claude 1151106cdf fix(bottom-bar-settings): stop reverting newly-pinned icons on drag
Replace `remember(pinned)` with a single `remember`, so the local
`items` MutableState isn't recreated each time the StateFlow emits.
Previously, when the user toggled an item ON, the resulting
recomposition swapped the local items state to a new MutableState
backed by `initialRows(newPinned)`. Drag-gesture callbacks captured
in earlier compositions kept writing to the old MutableState, and on
drag end they re-emitted a `pinned` list computed from that stale
state — dropping the just-pinned item.

Initialize once from `bottomBarItems.value` (already correct at first
composition) and let toggle / drag / restore-default be the only
writers. This makes the screen self-contained and removes the
position-vs-state race entirely.
2026-05-15 22:02:15 +00:00
Vitor Pamplona 0d90df2dbf Merge pull request #2916 from vitorpamplona/claude/fix-pulltorefresh-padding-eR5sf
Fix pull-to-refresh indicator positioning in DisappearingScaffold
2026-05-15 17:42:07 -04:00
Claude b9f98a4f5b refactor(feeds): collapse RefresheableBox overloads to a single PullToRefreshBox
The InvalidatableContent overload now delegates to the onRefresh overload,
so there is only one PullToRefreshBox/state-management path to maintain.

https://claude.ai/code/session_01MgnMaw8U83rXPSnoGF4KBY
2026-05-15 21:40:34 +00:00
Claude f52b3df4f8 fix: defer content subscription until repo event is loaded
When a Git Repository screen is opened via deep-link (naddr) the
addressable note exists but its event payload arrives later through
EventFinder. RepositoryContentSubAssembler.updateFilter checks
note.event and returns an empty filter when it isn't a
GitRepositoryEvent yet — and since ComposeSubscriptionManager only
re-invalidates on subscribe/unsubscribe, the now-stale empty filter
persists indefinitely, leaving the Issues/Patches tabs permanently
blank for cold-started repos.

Gate the subscription composable on event presence so the underlying
DisposableEffect fires fresh (running updateFilter again, this time
with the populated event) only after the repo event has loaded.
2026-05-15 21:31:32 +00:00
Claude 1d3c1edfea fix(feeds): offset PullToRefresh indicator below top bar
Inside a DisappearingScaffold the content fills the full screen height so
feeds can scroll behind the top bar. PullToRefreshBox anchors its indicator
at the top of the box, which placed it behind the top bar.

Read LocalDisappearingScaffoldPadding inside RefresheableBox and pass a
custom indicator with that top padding applied, so the spinner surfaces
just below the bar. Screens not inside a DisappearingScaffold see the
default 0dp padding and behave as before.

https://claude.ai/code/session_01MgnMaw8U83rXPSnoGF4KBY
2026-05-15 21:29:02 +00:00
Vitor Pamplona b6b1fb44d5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-15 17:22:31 -04:00
Claude 995741c2f1 chore: minor cleanups from audit round 3
- Drop two unused strings (git_repo_no_issues, git_repo_no_patches);
  the default FeedEmpty composable already provides a generic "Feed
  is empty" + refresh button which is sufficient.
- Import MaterialSymbol in GitRepositoryOverview so the LinkLine
  parameter type isn't a fully-qualified name, per CLAUDE.md style.
- Switch two vertical Spacer(Modifier.size(N.dp)) to
  Modifier.height(N.dp) — functionally identical but conveys intent.
2026-05-15 21:22:28 +00:00
Vitor Pamplona f38a27fc26 Merge pull request #2914 from vitorpamplona/claude/debug-desktop-release-83tpQ
Fix desktop app ProGuard build with Compose 1.11.0
2026-05-15 17:19:27 -04:00
Vitor Pamplona cfc86f1065 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-15 17:16:04 -04:00
Claude bfea285a6a fix: tighten GitStatusIndex initial-scan and pill warming UX
Two issues from a follow-up audit on the Git Repository screen.

- Move the initial cache scan into flow `onStart` so the
  newEventBundles collector is attached before scanning. Closes a small
  race window where status events emitted between scan completion and
  collect attachment would have been dropped.

- Distinguish "index warming up" from "no status exists" by typing the
  StateFlow as `Map?` (null until first scan completes). The pill now
  hides itself entirely while warming, instead of briefly rendering
  the `defaultIfMissing` fallback and then flipping to the real status.

Also drops the dead public `rememberLatestStatus` helper and the
unnecessary derivedStateOf scaffolding — at the scale this screen
operates (tens of pills), the straightforward map read is fine.
2026-05-15 21:14:16 +00:00
Vitor Pamplona 76d9b7b837 Activate Nests 2026-05-15 17:14:10 -04:00
Claude db564d9367 perf: index git status events and observe repo event reactively
Audit follow-up on the Git Repository screen.

- Add GitStatusIndex, a process-level Map<targetId, latest GitStatusEvent>
  fed by an initial scan on Dispatchers.IO and incrementally maintained
  from LocalCache.live.newEventBundles. GitStatusPill now does an O(1)
  map lookup instead of scanning the entire LocalCache on every issue/
  patch row at composition time on the main thread.

- Observe the GitRepositoryEvent reactively in the screen via
  observeNoteEvent so the title and Overview tab refresh when the event
  arrives after composition or when the maintainer publishes a new
  version.
2026-05-15 21:03:50 +00:00
Claude ffd64a2e70 fix(desktop): add project ProGuard rules to unblock release packaging
Compose Multiplatform 1.11.0 tightened its default ProGuard ruleset so
`proguardReleaseJars` now fails on unresolved references that 1.10.3
tolerated silently. The v1.09.0 release tag-build hit this on every
desktop leg (macOS DMG, Windows MSI, Linux DEB/RPM, Linux AppImage),
which is why only Android APKs and the macOS-arm64 / Linux Amy CLI
bundles made it onto the GitHub release.

Three groups of references were flagged:

  - Jackson kotlin-module 2.21.x value-class converters
    (`IntValueClassBoxConverter`, `ValueClassKeyDeserializer$*`, …)
    invoke `MethodHandle.invokeExact` with polymorphic signatures
    ProGuard cannot resolve against the abstract MethodHandle.
  - okhttp's optional TLS adapters (`okhttp3.internal.graal.*`,
    `BouncyCastle/Conscrypt/OpenJSSEPlatform`) reference classes from
    runtime backends that aren't on the desktop classpath; okhttp falls
    back to the JDK default in their absence.
  - JNA's `com.sun.jna.internal.Cleaner` inner classes touch synthetic
    `access$N` accessors that ProGuard's class-member analysis cannot
    follow.

Add `desktopApp/compose-rules.pro` with `-dontwarn` rules for each
bucket and wire it through `compose.desktop.application.buildTypes
.release.proguard.configurationFiles`. Verified locally by re-running
`:desktopApp:proguardReleaseJars` (now BUILD SUCCESSFUL) and
`:desktopApp:packageReleaseDeb` (gets past ProGuard; only fails on the
sandbox missing `fakeroot`, which CI installs in the existing
"Install RPM tooling" step).
2026-05-15 21:03:28 +00:00
Claude e69c166de2 feat: add dedicated Git Repository screen with Overview, Issues, Patches/PRs
Clicking a Git Repository note now opens a tabbed screen modeled after
the Community screen: Overview surfaces the description, links, topics,
maintainers, and personal-fork badge; Issues and Patches/PRs are
relay-backed feeds scoped to the repository address.

Adds NIP-34 status pills (Open/Merged/Closed/Draft) sourced from
kind 1630-1633 events, rendered on issue and patch cards both in the
feed and in the new screen. The short repository pill inside issue and
patch cards is now clickable and navigates to the new screen.
2026-05-15 20:29:01 +00:00
Vitor Pamplona f9bd4cd61c Merge pull request #2912 from vitorpamplona/claude/fix-pull-to-refresh-piOXw
Rename onRefresh callback to avoid shadowing parameter
2026-05-15 16:28:59 -04:00
Claude df58c666ae fix(home): restore pull-to-refresh on home feeds
The `onRefresh: () -> Unit` parameter in the second `RefresheableBox`
overload was shadowed by a local `val onRefresh` of the same name. The
recursive `onRefresh()` call inside the launched coroutine therefore
re-invoked the local wrapper instead of the caller's callback, so
pulling down on the home feed never actually triggered
`feedState.invalidateData()` or the DVM refresh.

Rename the local wrapper to `onRefreshWrapped` so the parameter remains
visible inside the lambda.
2026-05-15 20:21:24 +00:00
Vitor Pamplona 74c0d6906e v1.09.0 2026-05-15 15:43:47 -04:00
Vitor Pamplona 8947d9aaa3 Merge pull request #2910 from davotoula/fix/quoted-note-author-outbox
Resolve quoted notes via author's NIP-65 outbox
2026-05-15 13:03:58 -04:00
Vitor Pamplona 1d73b7a8e1 Merge pull request #2911 from davotoula/fix/nip05-verified-on-error-clean
Stop reporting verification errors as Verified
2026-05-15 13:03:28 -04:00
Vitor Pamplona 81bae46cd0 test(nests): skip AudioLatencyComparisonTest when -DnestsHangInterop unset
The pre-push hook runs the full `./gradlew test` suite, which doesn't
pass `-DnestsHangInterop=true`. Without that flag the sidecars don't
exist, so every test in this class blew up on `NativeMoqRelayHarness.shared()`
with `IllegalStateException`. Adds a `@BeforeTest` that calls
`assumeHangInterop()` and skips the case via JUnit's Assume — same
pattern `HangInteropTest`, `HangInteropReverseTest`, and the rest of
the `interop/native/` suite already use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:01:55 -04:00
Vitor Pamplona f512478bcc test(nests): sweep packet-loss rates + Linux docker harness for latency bench
`AudioLatencyComparisonTest`:
  - Three new scenarios: 10 %, 20 %, 30 % loss in addition to the
    existing clean and 5 %. The whole benchmark now sweeps a useful
    arc for finding the breakdown point.
  - Frame-count floor now scales with loss instead of a fixed 60 %
    threshold. QUIC streams are reliable so loss surfaces mainly as
    latency, but at high rates the retransmit + congestion-control
    feedback can eat into the 10 s window, so the floor relaxes to
    `max(50, (1 - 3*lossRate) * EXPECTED_FRAMES)`. The 50-frame
    absolute floor (= 1 s of audio) catches a catastrophic publisher
    failure without flapping on the loaded loss-path runs.

Linux benchmark harness (`nestsClient/tests/hang-interop/linux-bench/`):
  - `Dockerfile` for an arm64/amd64 image with JDK 21 + rustc 1.95 +
    cmake + the *-sys-crate build deps (libssl-dev, build-essential,
    pkg-config). Pins rustc 1.95 explicitly so moq-relay v0.10.25's
    Cargo.lock-requested `constant_time_eq@0.4.3` builds cleanly.
  - `run.sh` wrapper that:
    * builds the image,
    * unpacks a filtered tarball of the host repo into the container
      (mac host `nestsClient/build/` and `cargo target/` stay
      untouched),
    * symlinks `nestsClient/build` -> `/persistent-gradle-build` and
      `nestsClient/tests/hang-interop/target` ->
      `/persistent-cargo-target` so successive runs reuse compiled
      artefacts (~3-5 min cached vs ~7-15 min cold),
    * runs the gradle test invocation and surfaces the JUnit XML
      stats to `linux-bench/out/`.

Linux numbers observed (arm64 Docker on a MacBook M2, 10 s window):

  | Loss | Stack  | p50   | p95   | p99   | max   | Frames |
  |------|--------|-------|-------|-------|-------|--------|
  |  0 % | Kotlin |  61.1 |  63.1 |  65.5 |  79.1 |   573  |
  |  0 % | Rust   | 100.7 | 102.7 | 104.9 | 140.2 |   495  |
  |  5 % | Kotlin | 380.9 | 383.4 | 385.7 | 390.5 |   556  |
  |  5 % | Rust   |   0.7 |   7.3 |  23.2 |  84.2 |   500  |
  | 10 % | Kotlin | 120.5 | 122.4 | 124.0 | 132.6 |   568  |
  | 10 % | Rust   |   0.5 |  21.9 |  60.4 |  84.4 |   500  |
  | 20 % | Kotlin | 159.7 | 164.3 | 170.3 | 183.1 |   566  |
  | 20 % | Rust   |   1.6 |  61.3 | 101.9 | 144.9 |   499  |
  | 30 % | Kotlin | 181.0 | 184.2 | 188.1 | 194.0 |   566  |
  | 30 % | Rust   |   1.2 |  80.7 | 105.3 | 140.8 |   491  |

Notes for future readers: Rust's p50 collapsing to ~1 ms under loss
is the SUBSCRIBE-late-arrival signal, not "Rust is 100x faster" — by
the time the SUBSCRIBE lands at the publisher, the in-flight
encoder has moved past those early frames, so the listener starts
receiving in near-real-time. Read p95/p99/max for the per-frame
story (Rust expands 7 -> 22 -> 61 -> 80 ms p95 across loss rates).
Frame delivery is stable for both stacks even at 30 % loss on Linux,
unlike the macOS run which hits a 3.4 s Rust catastrophe at 30 %.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:01:55 -04:00
Vitor Pamplona 9fd8cddebf test(nests): one-way latency + 5 % packet-loss path in audio benchmark
Expands `AudioLatencyComparisonTest` from pure inter-arrival jitter
into a real one-way latency measurement against the same
`hang-publish` Rust reference, AND adds a loss-path variant gated
through `udp-loss-shim`.

`hang-publish` (Rust sidecar):
  - new `--log-send-times` flag. When set, the publisher prints
    `SEND frame=<N> send_t_us=<UNIX_EPOCH_MICROS>` to stdout for
    every frame, captured with `SystemTime::now()` immediately
    before `frame.encode(group)`. Lock + flush per line because a
    piped Rust stdout is fully-buffered by default and the JVM-side
    parser needs a live stream. Default off so existing scenarios
    (the I7 reconnect test in particular) keep their stderr-only
    RUST_LOG output untouched.

Kotlin test:
  - `TimestampingOpusEncoder` wraps `JvmOpusEncoder` and stamps an
    `Instant.now()` epoch-micros value per `encode(...)` call into a
    shared map keyed by a monotonic frame counter. encode runs
    immediately before `publisher.send` inside
    `NestMoqLiteBroadcaster`, so the captured timestamp is the
    closest cross-stack anchor we have to "moment the frame entered
    the publisher's outbound buffer" — same anchor the Rust side
    uses.
  - One-way latency = arrival epoch-micros − send epoch-micros per
    matched frame. Both sides read `CLOCK_REALTIME` on linux/macOS,
    so values can be compared directly without a sync handshake when
    the two processes share a host.
  - Frame matching uses `MoqObject.objectId` directly. The listener
    layer already synthesises objectId as a per-SUBSCRIPTION
    monotonic counter, so it equals the publisher's absolute frame
    index; a previous draft of this test did `groupId * 5 + objectId`
    and silently aligned against the wrong frames, producing
    negative latencies.
  - New `under_5pct_packet_loss_pacing_and_one_way_latency` scenario.
    Spawns TWO `udp-loss-shim` instances (one per publisher: the
    shim latches a single client on first datagram, so a shared shim
    would silently swallow the second publisher's traffic), each
    forwarding 1:1 to the moq-relay's UDP port modulo a 5 %
    bidirectional drop rate. The listener stays on the direct path
    so any latency growth comes from publisher-side retransmit /
    ack feedback, not listener-side loss.
  - Both Kotlin and Rust pinned to `FRAMES_PER_GROUP = 5` (Rust's
    default; Kotlin's default is 50). Matched so the per-group
    uni-stream open/close cost is the same on both sides — the
    comparison is about implementation, not about which stack picked
    which group size.

Observed (10 s window, localhost, MacBook Pro M2):

  ===== Audio publisher comparison: clean (loss=0%) =====
  Kotlin speaker     ttf=137 ms  inter-arrival p50/95/99/max=20.17/20.67/20.88/21.16 ms
                                  one-way        p50/95/99/max=80.60/81.31/81.74/83.91 ms
  Rust hang-publish  ttf=325 ms  inter-arrival p50/95/99/max=20.00/21.18/21.49/21.95 ms
                                  one-way        p50/95/99/max=100.32/101.45/102.04/168.58 ms
  ===== Audio publisher comparison: loss-5pct (loss=5%) =====
  Kotlin speaker                  one-way        p50/95/99/max=80.50/81.55/82.31/86.43 ms
  Rust hang-publish               one-way        p50/95/99/max=100.44/119.88/122.16/158.98 ms

  Reading: Kotlin matches Rust on the clean path within ~20 ms
  baseline (and actually sits below it), and absorbs 5 % loss with
  only +0.6 ms p99 growth vs Rust's +20 ms. The 80-100 ms baseline
  is moq-lite's group-buffer floor with 5-frames/group — protocol,
  not stack.

Asserts: each side delivers >= 80 % of expected frames on the clean
path, >= 60 % under loss, and clean-path median inter-arrival sits
in [15, 35] ms. Tail percentiles are printed, not gated.

Gated by `-DnestsHangInterop=true`; both sidecars (hang-publish,
udp-loss-shim) come from `interopBuildSidecars`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:01:55 -04:00
Vitor Pamplona e18e6c8399 test(nests): side-by-side audio pacing benchmark vs hang-publish
Adds `AudioLatencyComparisonTest` to the hang-interop suite — runs the
Amethyst Kotlin speaker AND the upstream `hang-publish` Rust sidecar
into the same local moq-relay, with one Kotlin listener subscribing to
both. Same room, distinct broadcast suffixes (per-publisher pubkey),
same Opus shape (440 Hz mono, 32 kbps, 20 ms frames), 10 s window.

What it measures (per publisher):
  - delivered frame count (sanity floor: 80 % of theoretical max)
  - inter-arrival p50 / p95 / p99 / max at the listener — the part
    the publisher stack actually controls (mic -> encoder ->
    publisher.send -> uni-stream open -> QUIC writer -> wire), since
    relay + receive path are identical
  - time-to-first-frame (informational; Rust pays process-spawn that
    JVM doesn't, so not asserted on)

What it does NOT measure: end-to-end glass-to-ear latency (would need
synchronised send-side timestamps from hang-publish, which means
modifying the Rust sidecar) and loss / congestion (no simulator on
the localhost link). For loss behaviour, see `quic/interop/`.

Asserts: each side delivers >= 80 % of expected frames and each
side's median sits in [15, 35] ms — catches a stalled publisher or
a no-pacing burst-mode publisher without depending on per-host tail
percentiles. Tail numbers are printed for human inspection.

Observed on this run:
  Kotlin speaker     frames=571  ttf=143.8 ms  p50=20.21  p95=20.63  p99=21.45  max=25.88
  Rust hang-publish  frames=495  ttf=326.0 ms  p50=19.99  p95=21.41  p99=22.18  max=26.72

Gated by `-DnestsHangInterop=true`; needs the cargo-built
`hang-publish` sidecar already produced by `interopBuildSidecars`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:01:55 -04:00
Vitor Pamplona 54f667f5bd fix(nests): unstick reconnecting speaker + listener interop on real relays
Two production-path bugs surfaced during a full interop sweep
(`./gradlew :nestsClient:jvmTest -DnestsInterop=true` +
`-DnestsHangInterop=true`) plus the toolchain / harness friction that
was hiding them.

Production fixes:

- ReconnectingNestsSpeaker's hot-swap path opens publishers via
  `openPublisherForHotSwap` instead of `startBroadcasting`, so the
  underlying `MoqLiteNestsSpeaker`'s state machine never made the
  Connected -> Broadcasting transition. The reconnect wrapper mirrors
  that state, so callers waiting on Broadcasting (the VM,
  `connectReconnectingNestsSpeaker` interop tests) hung on Connected
  forever. Adds `HotSwappablePublisherSource.reportBroadcasting(isMuted)`,
  implemented in `MoqLiteNestsSpeaker`, and the hot-swap pump now
  calls it each iteration so a JWT-refreshed session re-enters
  Broadcasting too.

- The stale-group filter on listener reconnect assumes monotonic
  group lineage across publisher session-restarts (true for
  ReconnectingNestsSpeaker, which seeds each new publisher with the
  prior `nextSequence`; false for external publishers like
  kixelated's hang-publish reference, which mints a fresh state on
  every reconnect and restarts at 0). Without compensation, the
  watermark from a session-1 max ~18 dropped the entire post-restart
  stream from session 2. The collect now resets the watermark on the
  first object of a new subscription when its group id arrives well
  below the current watermark — a publisher-restart signal that a
  relay-cache replay (which carries exactly the prior max) wouldn't
  produce.

Test harness fixes — these are what blocked the suites from running
at all on a clean Apple-Silicon box:

- NostrNestsHarness: the cloned `docker-compose-moq.yml` declares no
  `depends_on`, so on a cold `up -d` moq-relay raced moq-auth's Node
  boot, got `Connection refused` on its JWKS GET, exited with no
  retry, and every later QUIC handshake failed with "read loop exited
  (socket closed or peer closed)". Gate on moq-auth's /health first,
  then idempotent `up -d moq-relay` so the relay always boots against
  a live auth sidecar.

- nestsClient/build.gradle.kts: cargo builds inside the hang-interop
  task panic under CMake 4.x because `audiopus_sys` / rustls' aws-lc-sys
  ship a `CMakeLists.txt` that predates CMake 4's minimum-version
  floor. Set `CMAKE_POLICY_VERSION_MINIMUM=3.5` on each cargo Exec.

- JvmOpusEncoder + the hang-interop Test task: opus-java 1.1.1's
  bundled `natives/darwin/libopus.dylib` is x86_64-only, so its
  in-jar loader reports unsupported on Apple Silicon. Probe a small
  set of canonical system libopus locations (`brew install opus` on
  macOS, distro paths on linux) and `System.load` by absolute path so
  the symbols are in the process when JNA's lazy `Opus.INSTANCE`
  init falls back to RTLD_DEFAULT. Gradle also threads
  `/opt/homebrew/opt/opus/lib` onto `jna.library.path` for
  completeness; both paths are no-ops where the in-jar binary
  already loads.

After these:
- :nestsClient:jvmTest -DnestsInterop=true             — 312/312, 0 fail
- :nestsClient:jvmTest -DnestsHangInterop=true         — 312/312, 0 fail
- quic/interop/run-matrix.sh -s aioquic                — 19 passed,
  0 failed, 3 unsupported (E/V2/CM are documented amethyst gaps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:01:55 -04:00
davotoula 8d225c355e Code review:
- reject ill-formed localpart dots and IP literals in parse
- avoid Pair allocation in Nip05Id.parse
2026-05-15 17:17:43 +02:00
davotoula e79755dab3 fix(nip05): stop reporting verification errors as Verified 2026-05-15 17:17:43 +02:00
davotoula c9087fb041 Code review:
-  collapse author-hint null check in consume(NEvent)
2026-05-15 17:02:25 +02:00
davotoula a36873b15c fix(quote): resolve quoted notes via author's NIP-65 outbox 2026-05-15 16:53:39 +02:00
Claude 5e65bccb54 feat: modernize Git Repository, Issue, and Patch card design
Replace plain text layout in NoteCompose Git renderers with a unified
card style: icon badges, type chips, a compact repository header pill,
and icon-led link rows for web/clone URLs. Patches now surface the
short commit hash in the header.
2026-05-15 14:37:19 +00:00
Vitor Pamplona 3a7e9a90a9 Merge pull request #2908 from nrobi144/fix/account-security-hardening
fix(desktop): Account security hardening — NWC to keychain, single source of truth
2026-05-15 07:08:12 -04:00
nrobi144 325a1f6f6d fix: address code review — NWC key cleanup, CancellationException rethrow
- removeAccountFromStorage now deletes nwc_<npub> keychain entry
  (fixes orphaned wallet key on account removal)
- Rethrow CancellationException in loadSavedAccount and loginWithKey
  catch blocks (fixes broken structured cancellation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 11:33:16 +03:00
nrobi144 46caa4d795 fix(desktop): harden account security — NWC to keychain, single source of truth
CRITICAL: Move NWC wallet secret from plaintext nwc_connection.txt to OS
keychain. The NWC secret is a private key that can authorize Lightning
payments — storing it in plaintext allowed any process to steal funds.

Security fixes:
- NWC secret stored in OS keychain as "nwc_<npub>" (per-account)
- accounts.json.enc is now the sole source of truth for cold boot
- Eliminate bunker_uri.txt, last_account.txt, nwc_connection.txt
- Legacy files deleted on first startup (one-time cleanup)
- logout(deleteKey=true) now removes account from accounts.json.enc
- Corrupted accounts.json.enc backed up as .corrupt.<timestamp>

Cold boot rewrite:
- loadSavedAccount() routes by SignerType from accounts.json.enc
- No longer reads stale bunker_uri.txt (fixes nsec→bunker confusion)
- No longer reads last_account.txt (uses activeNpub from metadata)

Multi-account improvements:
- NWC connections are per-account (switch account = switch wallet)
- Each account type (Internal/Remote/ViewOnly) loads correctly
- saveBunkerAccount() no longer writes to bunker_uri.txt

Updated 8 existing test files to use accountStorage instead of
writing legacy files directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 11:29:42 +03:00
David Kaspar 710482e91d Merge pull request #2909 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-15 10:01:46 +02:00
Crowdin Bot 3ab0ba2c6a New Crowdin translations by GitHub Action 2026-05-15 08:00:37 +00:00
davotoula 4cdf8c3ec2 i18n: translate favorite-DVMs empty-state and CTA strings
Adds cs-rCZ, pt-rBR, sv-rSE, de-rDE translations for the three new
keys from the favorite-feeds "Add more…" flow:
favorite_dvms_add_more, favorite_dvms_empty_step1,
favorite_dvms_empty_step2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:56:50 +02:00
Vitor Pamplona 49e4447b90 Merge pull request #2907 from vitorpamplona/claude/improve-compose-settings-NKNWr
Refactor ComposeSettingsScreen to use unified tile components
2026-05-14 20:05:38 -04:00
Vitor Pamplona c571a24fe8 Merge pull request #2906 from vitorpamplona/claude/fix-nests-quic-audio-delay-MHzZL
Fix QUIC handshake timeout, IPv4 preference, and SNI handling
2026-05-14 20:05:20 -04:00
Claude f8336b07e7 refactor: modernize Compose Settings screen layout
Switch the Compose Settings screen from the tightly-spaced legacy
SettingsRow list to the card-based SettingsSection / SettingsControlRow
pattern already used by SecurityFiltersScreen. Adds leading icons,
proper breathing room between items, and full-row tap targets for the
switches.
2026-05-14 23:03:00 +00:00
Claude 5d406c5998 fix(nests): address self-audit findings on the interop transport fixes
- QuicWebTransportFactory: re-throw CancellationException from the bounded
  handshake instead of wrapping it in WebTransportException, so a
  parent-scope cancellation no longer breaks structured concurrency.
  Mirrors the existing handling on the post-CONNECT path.
- NostrNestsHarness: correct two misleading comments — the dev endpoint's
  IPv4 guarantee comes from QuicWebTransportFactory's resolve step, not
  from `localhost` itself; and DEFAULT_REVISION is documented as tracking
  `main` (a known drift risk against the pinned moq-relay), not pinned.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
2026-05-14 23:02:52 +00:00
Claude ec92a82e4e fix(nests): pin the interop harness's moq-relay to the version moq-auth expects
The local Docker interop stack builds moq-relay from a `./moq` checkout
that NostrNestsHarness cloned at unpinned `main` (~0.10.25+), while
nostrnests's moq-auth sidecar mints tokens for moq-relay 0.10.10 — the
version nostrnests pins in their own nests-relay/Dockerfile.relay. The
JWKS/JWT-signature handling drifted across those releases, so the relay
rejected every connection with `failed to decode the token` even though
the QUIC/TLS/WebTransport handshake completed cleanly.

Pin DEFAULT_MOQ_REVISION to the moq-relay-v0.10.10 release tag so the
harness builds the relay moq-auth was written against, and fetch tags so
the tag is checkout-able on a pre-existing clone.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
2026-05-14 22:51:37 +00:00
Claude 692a385f5b diag(nests): include the resolved socket target in handshake failures
A stalled/failed QUIC handshake gave no hint whether the socket connected
over IPv4 or fell into the ::1 blackhole. The HandshakeFailed messages
now carry `socket=<addr>:<port> sni=<host>` so the failure itself shows
which address resolvePreferIpv4 picked.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
2026-05-14 22:02:27 +00:00
Claude 6a60918cc6 fix(nests): connect over a resolved IPv4 address while keeping hostname SNI
After switching the dev harness to `localhost`, the QUIC handshake still
stalled and the relay logged nothing at all — packets weren't reaching
it. `localhost` resolves to ::1 first on most systems, and IPv6 loopback
isn't reliably routable (Docker's IPv6 UDP port-forwarding silently
blackholes), so the UDP socket was connecting into a hole.

QuicWebTransportFactory.connect now resolves the authority host to a
concrete address preferring IPv4 for the UDP socket target, while still
passing the original hostname as the TLS SNI server name — so SNI keeps
matching the server certificate and stays a legal RFC 6066 host_name.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
2026-05-14 21:51:35 +00:00
Vitor Pamplona 18c669e7bb Merge pull request #2905 from vitorpamplona/claude/update-changelog-efQ9o
Release v1.09.0: We are going crazy
2026-05-14 17:42:11 -04:00
Claude ed1f10c071 fix(quic): omit IP-literal SNI, bound the handshake, fix dev harness host
Three findings from the local nests interop hang (relay logged "Illegal
SNI extension: ignoring IP address presented as hostname"):

- TlsClientHello sent the connect host verbatim as TLS SNI, including
  IP literals. RFC 6066 §3 forbids IP literals in server_name; a strict
  relay (rustls) discards the extension, ends up with no SNI to select
  its certificate on, and the handshake stalls. Both ClientHello
  builders now omit server_name when the host is an IP literal
  (new isIpLiteralHostname predicate).

- QuicWebTransportFactory.connect never bounded awaitHandshake() — only
  readConnectResponse had a timeout — so a stalled handshake hung
  connect() indefinitely. It now fails fast after connectTimeoutMillis
  with a message naming the likely cause.

- NostrNestsHarness pointed moqEndpoint at 127.0.0.1, which forced the
  IP-literal SNI path against the dev relay. Switched to localhost: a
  valid hostname that matches the relay's generated dev cert and still
  resolves to loopback.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
2026-05-14 21:41:07 +00:00
Claude 84c5013d10 docs: de-jargon v1.09.0 changelog for outside readers
Replaces internal class names and implementation jargon with
reader-facing descriptions, and drops a few pure-internal lines
(AccountManager migration, SQLite parity matrix, remember-wrapper
removal, ProGuard/R8 keep rules).

https://claude.ai/code/session_01PyGyMpa9XvTHrB4iqZX5oZ
2026-05-14 21:39:43 +00:00
Claude 806d357569 diag(nests): explain why a WebTransport CONNECT failed instead of bare :status=0
QuicWebTransportFactory threw `:status=0` for every distinct CONNECT
failure mode — relay never answered, relay FIN'd the request bidi with
no H3 response, relay sent a HEADERS frame lacking :status, or the whole
connection was torn down by a CONNECTION_CLOSE. The four cases need
different fixes but produced identical, undiagnosable errors.

readConnectResponse now records bytes/H3-frames seen on the request
stream and reports which of those cases occurred; the thrown exception
also carries a connection-state snapshot (conn.status, closeReason,
closeErrorCode, requestStreamClosed) captured before driver.close(), so
a relay CONNECTION_CLOSE (e.g. H3_SETTINGS_ERROR from a rejected SETTINGS
frame) is distinguishable from a request-stream-only close.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
2026-05-14 21:23:24 +00:00
Claude d32b0fdb4c docs: scope-accuracy audit pass on v1.09.0 changelog
Walks the full v1.08.0..HEAD non-merge history and corrects items that
were absent or under-represented relative to what shipped:

- Fixes inaccuracy: DLNA casting was removed before release; casting is
  Chromecast-only on the Play build.
- Promotes Interest Sets (NIP-51) from a fix-line to a feature.
- Adds configurable bottom nav, drawer reorganization, autoplay-videos
  setting, lifecycle-aware subscriptions, adaptive video cache, account
  settings republish-to-new-relays, Tor clearnet fallback, and more.
- Expands NIP-34 (PRs, Status, Repo State), the custom secp256k1 entry
  (Kotlin core + C/JNI accelerated path), Namecoin, and Marmot scope.

https://claude.ai/code/session_01PyGyMpa9XvTHrB4iqZX5oZ
2026-05-14 21:23:00 +00:00