Commit Graph
15076 Commits
Author SHA1 Message Date
Vitor PamplonaandClaude Opus 4.8 48333f0007 refactor(dm): drop dead pagination code with no production caller
Removes machinery that ships but is unreachable from any live path:

- WindowLoadTracker's REQ-aware backstops (silence + connect-grace). All
  three live-tail managers construct it with the default tracksReqSends =
  false, so onReqSent/reqSentAt/silencedOut/connectStalled and the
  onAbandoned reporting could never fire. Only WindowLoadTrackerSilenceTest
  exercised them, so it goes too. accountedFor collapses to "settled".
- trackingListener's onEachEvent param — no caller ever passed it.
- UntilLimitPager.isArmed() / activeRelays() — only the unit test called
  them; the pager uses armedRelays() in production.
- Orphaned string chats_load_entire_history, left behind when the
  "load entire history" button was removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:44:56 -04:00
Vitor PamplonaandGitHub 5a5a4a9c27 Merge pull request #3136 from davotoula/feat/hide-reposts-of-unsupported-kinds
Hide reposts whose boosted kind is unsupported
2026-06-05 15:12:43 -04:00
davotoula 7672282892 refactor: share isRenderableRepost via commons, apply on desktop
Move isRenderableRepost() (and its test) from amethyst ui/dal into
commons/ui/feeds so both platforms use one implementation, then point
desktop's isFeedNote() at it.
2026-06-05 20:47:09 +02:00
davotoula dc44802beb Code review:
- dedup boostedKind and simplify isRenderableRepost
2026-06-05 20:46:35 +02:00
davotoula 58e4a7c610 feat(feed): hide reposts whose boosted kind is unsupported 2026-06-05 20:46:13 +02:00
Claude b672611c52 feat(dm): log the NIP-42 AUTH handshake in the DM diagnostics
The DM trail showed REQ + CLOSED/NOTICE/OK-fail but was blind to the entire
NIP-42 handshake — incoming AUTH challenges hit `else -> {}`, the AUTH send is
not a REQ so onSent skipped it, and the auth OK(true) was filtered out by the
OK(fail)-only guard. So a CLOSED 'auth-required' followed by a re-REQ was
unattributable: you couldn't tell whether the relay challenged, whether we
signed+sent AUTH, whether it was accepted, and whether the re-REQ was the
post-auth syncFilters or just a blind retry.

Add three lines under the DMPagination tag (DM relays only, debug):
- `AUTH challenge <- relay '<challenge>'`  (incoming AuthMessage)
- `AUTH -> relay (id ...)`                 (our AuthCmd reply; id remembered)
- `AUTH accepted|REJECTED <- relay '...'`  (the OK matched to that auth id)

With these, the post-CLOSED sequence reads end to end: challenge → AUTH → accept
→ re-REQ (→ events/EOSE), so an auth-walled DM relay's load can be confirmed or
pinned to the exact link that breaks.
2026-06-05 17:55:28 +00:00
Claude 82ffd4c16b test(dm): cover the WindowLoadTracker idle backstop + its heard-from gate
Adds WindowLoadTrackerIdleTest, the one WindowLoadTracker backstop that had no
coverage (silence and connect-grace are already pinned by
WindowLoadTrackerSilenceTest):

- idle completes a window when a relay streams stored events but never EOSEs,
  once the stream goes quiet for idleTimeout (the "every relay settled" path can
  never finish such a relay).
- the idle gate holds the window open while a still-pending relay has never been
  heard from, so a slow connect isn't mistaken for a quiet stream; once that
  relay delivers anything and goes quiet, idle then completes it.

Real-time tests with a short idleTimeout, matching the silence suite's style.
2026-06-05 17:35:23 +00:00
Claude 4fc24950e3 refactor(dm): cleanup + DRY/test the relay-reach gap predicate
Audit follow-up.

- Remove the 12 history-card strings (chats_history_older / all_caught_up /
  reached_start / relay_sync / subtitle{,_no_date} / waiting / relays_title /
  relay_back / incomplete{,_sub} + the chats_history_relays plural) from
  amethyst's default strings.xml: they moved to commons composeResources with
  the card and have zero remaining amethyst references. They were branch-new and
  not yet translated, so removing the default key is a clean, orphan-free delete.
  Kept chats_history_proto_* (still the card's protocolName) and chats_reply_*.

- Extract the "does this relay's reached cursor fall in this gap" check, which
  was duplicated (with off-by-one-prone >/<= boundaries) between marker placement
  (RelayWindowLimitMarkers) and the paging driver (RelayWindowLimitSentinels),
  into a single pure reachedFallsInGap(); both now call it so they can't disagree
  about which gap a cursor lives in. Add RelayReachMarkerTest pinning every
  boundary (newer strictly >, older inclusive <=, null ends).

- Fix a garbled comment in BackwardRelayPager.onSilenced.
2026-06-05 17:19:06 +00:00
Claude f631b0fbf1 docs(dm): reflect the commons UI extraction in the design doc
The relay-reach markers and the history status card now live in commons/ui/feeds
(shared Android + Desktop), with the card taking an injected date formatter.
Update the component map's UI section accordingly.
2026-06-05 16:59:49 +00:00
Claude ace9d20690 refactor(dm): extract the history status card to commons (shared with desktop)
Phase 2 (UI), stage 2. Move DmHistoryLoadingCard + its tap-through per-relay
dialog + the historySubtitle/incompleteSubtitle helpers out of amethyst into
commons/commonMain (com.vitorpamplona.amethyst.commons.ui.feeds), so the same
"older history / all caught up / some relays didn't respond" boundary card can
back any per-relay BackwardRelayPager feed on Android and Desktop.

The card's localized date formatting (SimpleDateFormat/Locale) can't live in
commons commonMain (it targets iOS/linux/macOS, no java.*), so it's injected as
a formatReachDate: (epochSeconds) -> String lambda — the platform that renders
the card supplies its native formatter, no i18n regression. Android passes
formatHistoryReachDate (new HistoryDateFormat.kt). The dialog's per-relay reach
now uses that same month-precision formatter (was "MMM d, yyyy", now "MMM yyyy")
— a negligible cosmetic change.

Its ~11 strings move to commons composeResources, including the module's first
<plurals> (chats_history_relays). The three Android call sites (ChatroomView,
ChatroomListFeedView, LoadingReplyNote) now import the shared card/helpers and
pass the formatter; no behaviour change. The old amethyst string copies are left
in place (harmless, separate resource namespace) for a later cleanup pass.
2026-06-05 16:58:34 +00:00
Claude c3ed7e65c9 refactor(dm): extract relay-reach markers to commons (shared with desktop)
Phase 2 (UI), stage 1. Move the per-relay reach markers — RelayReachState,
RelayReach, RelayWindowLimit, RelayWindowLimitSentinels (the hoisted,
visibility-driven paging driver), RelayWindowLimitMarkers, and RelayReachMarker
— out of amethyst into commons/commonMain (com.vitorpamplona.amethyst.commons.ui.feeds)
so the Desktop chats UI (and any feed) can render the same in-stream paging
progress, not just Android.

De-Android-ified: the one Android string (chats_history_relay_sync) becomes a
CMP composeResources string in commons; the two theme constants (DividerThickness,
HalfPadding) are inlined (0.25.dp / padding(5.dp)) so the shared component carries
no app-theme dependency. Logic is otherwise byte-identical. The Android
conversation + rooms-list views now import the shared version; no behaviour change.
2026-06-05 16:50:07 +00:00
Claude 3871d3bd1d docs(dm): reflect the BackwardRelayPager extraction in the design doc
The per-relay paging primitives moved to quartz (nip01Core/relay/client/paging,
jvmAndroid source set) and the three history managers now delegate their shared
bookkeeping to BackwardRelayPager. Update the architecture note and the
component map (new toolkit location + the two new quartz tests) accordingly.
2026-06-05 16:28:29 +00:00
Claude 1c7e073b48 refactor(dm): wire the three history managers onto BackwardRelayPager + tests
Step 3+tests of the pagination generalization. The gift-wrap, conversation
NIP-04, and rooms-list NIP-04 history managers each reimplemented the same
per-relay cursor / in-flight / stall / exhausted / display-flow bookkeeping;
they now delegate all of it to the shared BackwardRelayPager and keep only
what is genuinely theirs: building the protocol's REQ filters, the relaysFor
lookup, and forwarding subscription callbacks. ~550 lines of duplicated logic
removed; the public API (loadingMore/exhausted/relayCount/stalledCount/
reachedBack/relayProgress, advance/advanceAll) is unchanged, so the UI is
untouched. Behaviour-preserving.

Tests (quartz jvmAndroidTest):
- BackwardRelayPagerTest drives the engine's callbacks directly and pins the
  logic that backed the bugs in this branch: empty page -> done -> caught up;
  a CLOSED / cannot-connect relay -> stalled -> exhausted-but-INCOMPLETE
  (stalledCount > 0, not "all caught up"); re-advance clears a stall; the
  reached cursor is the deepest across relays; a done relay won't re-advance;
  advanceAll arms only not-done relays; switching the active key repoints the
  display flows and restores a backgrounded key's terminal state.
- UntilLimitPagingRelayTest drives a real NostrClient against the in-process
  relay (geode) to pin the wire contract the design rests on: a backward
  until+limit walk returns each event exactly once (no re-download), newest
  first, capped at the limit, with an empty page + EOSE as the gap-proof stop.
2026-06-05 16:23:30 +00:00
Claude 7ef6224c19 refactor(dm): extract per-relay paging primitives to quartz + add BackwardRelayPager
Step 1+2 of generalizing the DM history pagination into a reusable toolkit.

Move the four transport-agnostic primitives out of the amethyst module into
quartz's jvmAndroid source set, new package
`nip01Core.relay.client.paging`: UntilLimitPager, PerRelayLoadTracker,
WindowLoadTracker, RelayPagingProgress. They were already pure Kotlin (no
Android deps); jvmAndroid keeps their java.util.concurrent / @Synchronized
concurrency without a KMP-atomics rewrite, while making them visible to
amethyst, desktop, and quartz's jvmAndroidTest (geode in-process relay) for
the integration tests to come.

Add BackwardRelayPager<K>: the generic per-relay backward-pagination engine
that collapses the ~80%-identical pager+tracker+status+exhausted bookkeeping
the three DM history loaders each reimplement. It owns the cursors, in-flight
+ silence tracking, stalled set, pinned floor, and the display StateFlows
(relayProgress / exhausted / reachedBack / relayCount / stalledCount); the
caller supplies only the filter builder, the subscription wiring, and a
relaysFor(key) lookup. Not yet wired into the managers — that swap is step 3.

Pure relocation + new component; no behavior change. UntilLimitPagerTest and
WindowLoadTrackerSilenceTest stay in amethyst (they use JUnit) with explicit
imports added, and both still pass against the relocated classes.
2026-06-05 14:09:12 +00:00
Claude 0e4d2e96bc docs(dm): rewrite the DM pagination design doc to match the code
The doc had drifted from the implementation on two material points:

- "Update 3" claimed the rooms-list and gift-wrap *history* managers still use
  the round model. They were both migrated to the per-relay until+limit model
  (commits 60b8629a, 9f0ecd54); all history paging is now per-relay. The round
  model (WindowLoadTracker) survives only as the live-tail completion barrier.
- The rooms-list "stall-gate" it described was removed (commit 98fb8720); the
  rooms list now pages to exhaustion off marker visibility like the convo.

Restructured so the current architecture is authoritative and up front (two
layers, the two completion models and where each lives, the marker/sentinel
driver, per-relay NIP-04 filter scoping, terminal-state split, reply
placeholder, diagnostics, Tor self-heal), with a component map and review
notes. The superseded time-slice and round-model history are kept clearly
labelled under "Design evolution (historical)".
2026-06-05 13:19:24 +00:00
Claude 79f237ff0b Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-05 12:33:34 +00:00
Claude 813110cc29 fix(dm): distinguish 'caught up' from 'couldn't reach relays' in history/reply cards
The terminal state of both DM history cards equated two very different things:
a relay that genuinely bottomed out (empty page = done) and one that merely
stalled (auth CLOSE / offline / 15s silent). `exhausted` flips true when every
relay is done OR stalled, and the card rendered that unconditionally as "All
caught up" — so a chat whose messages sit on a stalled relay claimed completion
while a lot of history was still unfetched. The reply placeholder had the same
flaw and, worse, collapsed to a bare 👀 (post_not_found_short) that told the
user nothing.

Split the terminal state by stalledCount:
- caughtUp (all done): keeps "All caught up" and the lingering collapse.
- incomplete (>=1 stalled): "Some relays didn't respond · N unreachable",
  error-coloured glyph, stays put (no collapse).

Both cards are tappable into the existing per-relay popup, so the user can see
exactly which relays stalled (… in error colour) vs reached the bottom (✓).

The reply placeholder now says "Couldn't find this message" with an honest
subtitle — "N relays unreachable · tap to see which" when stalled, or "Searched
every relay · tap to see" when it genuinely isn't in history — instead of 👀.
2026-06-05 00:53:55 +00:00
Claude 61324aa7f0 feat(dm): tap the history card to see per-relay window positions
The card summarized progress but hid the per-relay detail that relayProgress
already carries. Make the card tappable: it opens a popup listing every relay
with its state (✓ done · … stalled · ↓ reaching) and how far back it has paged
('back to <date>'), deepest-reaching first. Passes each loader's relayProgress
through to the card; empty progress keeps the card non-interactive.
2026-06-04 23:04:27 +00:00
Claude a2b7eac701 feat(dm): show 'waiting on N relays' on the paused history card
The card's count was loadTracker.count() = relays in-flight, which drops to 0
the moment they all stall — so historySubtitle hit its relayCount<=0 guard and
showed just the bare protocol tag ('NIP-17' / 'NIP-04'), with no hint of how
many relays it was waiting on.

Expose a stalledCount (not-done relays that can't be reached right now) from all
three history loaders and render it on the paused card as 'waiting on N relays'
when nothing is in flight. Active fetching still shows 'N relays'; a parked-but-
reachable protocol still shows just the tag (it isn't waiting on anything —
it resumes on scroll).
2026-06-04 22:57:37 +00:00
Claude b9c2c646fd feat(dm): log a 'window settled' line when every relay is done or stalled
The demand-driven rewrite dropped the round model's 'load done: all relays'
completion line, leaving no aggregate signal for 'this is as far as history
goes right now'. Log one per protocol on the exhausted false->true edge, with
the done vs stalled relay breakdown, restoring that diagnostic.
2026-06-04 22:48:06 +00:00
Vitor PamplonaandClaude Opus 4.8 0394ec2a75 fix(dm): drive window-limit paging off viewport visibility, not row composition
The per-relay window-limit sentinel placed its advance() effect inside the
gap row that currently hosts the marker, so its identity rode that row. Any
feed reorder (a live DM bumping a room, or a slow relay dribbling a history
page) moved the gap to a different row, tore the keyed LaunchedEffect down and
recreated it, and re-fired advance() on a static screen — re-arming stalled/
auth relays into a 15s silence-watchdog storm and risking an unprompted
page-back for delivering relays.

Hoist the driver above the list: RelayWindowLimitSentinels now holds one
stable effect per non-done limit (keyed by lim.key) that watches the
LazyListState and fires advance() only when the marker's gap is among the
currently visible rows AND it just scrolled into view OR its reached cursor
moved (a page landed). A reorder that keeps the marker on the same side of the
fold changes neither, so it no longer re-pages. RelayWindowLimitMarkers is now
pure UI. Wired in the rooms list (inline) and the conversation (via a new
sentinels slot on ChatFeedView).

Verified on device: static rooms list drops from a perpetual ~15s re-fire/
silence storm to 2 silence events and only legit cursor-moved advances, while
demand-driven paging (page back while the marker is visible) is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:34:43 -04:00
Claude a4368c403a fix(dm): stop the history loading card flickering between back-to-back pages
PerRelayLoadTracker dropped the loading flag the instant in-flight emptied, so
a relay paging page-after-page (each page settles, then the marker fires the
next) flipped loading true->false->true every page — and the card's icon hard-
cut spinner -> paused-glyph -> spinner, a visible blink per page.

Let loading=false linger ~600ms after the last page settles; cancel the linger
the moment the next page starts. Back-to-back pages now show a steady spinner;
the card still flips to paused/done once paging genuinely stops.
2026-06-04 22:26:19 +00:00
Claude c4b1eaaa3d fix(dm): pin the history floor so un-delivered relays don't re-page; add sentinel/bootstrap trace logs
The floor (now - 7d) was recomputed on every call, so it drifted forward
over time. For a relay that hadn't delivered yet (reachedUntil == null),
reachedUntilFor returned that drifting floor, so its marker's reached cursor
kept changing, the sentinel's LaunchedEffect(reachedUntil) key changed, and
it re-fired advance() — observed as rooms.nip04.history re-advancing a silent
vitor.nostr1.com every ~15s, with the retry's 'until' moving NEWER each time.

Pin the floor once per account/conversation for the session (matching the
pre-rewrite behavior) in all three history loaders, so an un-delivered relay
parks at a stable cursor and its sentinel fires once.

Also add diagnostics to catch this class of bug quickly: one log line per
marker sentinel fire (key + reachedUntil — a loop repeats the same key, a
drift shows a moving cursor) and one per advanceAll (empty-feed bootstrap).
2026-06-04 21:41:07 +00:00
Vitor PamplonaandGitHub 9c175259c6 Merge pull request #3135 from davotoula/feature/video-swipe-brightness-volume
Fullscreen swipe controls for brightness & volume
2026-06-04 17:35:22 -04:00
davotoula 54caab3520 Sync video mute with fullscreen volume swipe 2026-06-04 23:19:42 +02:00
davotoula f2df23cfa6 Code review:
- fix(player): harden fullscreen swipe controls and brightness lifecycle
2026-06-04 23:19:28 +02:00
davotoula 5edfe90321 feat(player): enable brightness/volume swipe in fullscreen video 2026-06-04 23:19:08 +02:00
Claude 9ac835557a fix(dm): stop window-limit sentinels re-paging on relay connection churn
The per-relay sentinel keyed its LaunchedEffect on (reachedUntil, state).
The state component meant every REACHING<->STALLED transition restarted the
effect and re-issued advance(), so a flaky or auth-walled relay's reconnect/
timeout churn re-paged the window on a completely static screen (observed:
vitor.nostr1.com + auth.nostr1.com re-REQing on every ping timeout / auth
CLOSE with no user interaction).

Key the sentinel on reachedUntil ALONE — a landed page is the only thing
that should pull the next one. A stalled relay now parks until its cursor
moves or its marker is scrolled back into view (one retry, not a loop).

Also: the empty-feed bootstrap now triggers on FeedState.Empty only (not the
transient Loading that navigation flashes through) and is debounced, so
re-opening Messages / a conversation that already has content no longer kicks
a one-round advanceAll across every relay.
2026-06-04 21:15:57 +00:00
Claude eaddceba16 fix(dm): harden pager loop guard and foreground-only status writes
Audit follow-ups to the demand-driven paging change:

- UntilLimitPager.onEose: require the reached cursor to move strictly
  older each page. A misbehaving relay that returns events but none older
  than already reached would otherwise pin the cursor and the on-screen
  sentinel would re-request the same window forever; treat it as the bottom.

- updateStatus (gift-wrap + rooms-list NIP-04): only write the shared
  display StateFlows for the foreground account, mirroring the existing
  exhausted guard, so a background account's late EOSE can't clobber the
  on-screen relay count / reached-back / per-relay markers.

- Add UntilLimitPagerTest covering the requested/reached split, park-on-
  EOSE, done-on-empty, the strict-older guard, and per-relay independence.
2026-06-04 20:30:19 +00:00
Claude d20b02047b feat(dm): demand-driven per-relay history paging via window-limit sentinels
Replace the proactive walk-to-exhaustion with on-demand, per-relay paging
driven by on-screen window-limit markers, so history loads only while the
user is looking at a relay's frontier and a spam-dense relay never floods.

UntilLimitPager: split the cursor into 'requested' (drives the REQ; moves
ONLY on advance()) and 'reached' (oldest delivered; for markers). Leaving
requested untouched on EOSE is what parks a relay — no auto-continuation.

PerRelayLoadTracker (new): tracks which relays have a page in flight (for
the spinner) with a silence watchdog to park relays that accept a REQ then
go quiet. Replaces the window/round bookkeeping for the history loaders.

All three history loaders (gift-wrap account-wide, rooms-list NIP-04,
conversation NIP-04) rewritten: onEose parks instead of self-continuing;
advance(relay) steps one relay one page; advanceAll() bootstraps the
empty/initial boundary; exhausted = every relay done or stalled.

UI: RelayWindowLimitMarkers renders each relay's marker at its reached
depth AND acts as its load sentinel — while the marker is composed
(on/near screen) it pulls that relay's next page (LaunchedEffect keyed on
the reached cursor, so it keeps going page after page while visible) and
stops when the marker scrolls off or the page fills the screen. Wired into
both the conversation (gap markers) and the rooms list (interleaved
between rooms); a BootstrapHistoryWhenEmpty drives advanceAll while the
feed has nothing to scroll.

Removes the round/give-up machinery's last users: drop loadMore/
loadEverything and the scroll-driven WidenHistoryWhen.
2026-06-04 20:08:54 +00:00
Vitor PamplonaandGitHub 56240c10d2 Merge pull request #3134 from vitorpamplona/claude/compassionate-cannon-3mX7Y
Fix cache-pruning invariant for onchain zaps and nutzaps
2026-06-04 15:08:23 -04:00
Claude d7e21fb3bd Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-04 19:04:22 +00:00
Vitor PamplonaandGitHub c6be3ee3a3 Merge pull request #3118 from mstrofnone/docs/namecoin-design-refresh
docs(namecoin): refresh NIP-05 design doc to match current main
2026-06-04 15:03:37 -04:00
Vitor PamplonaandGitHub 8a156261cd Merge pull request #3132 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-04 15:03:24 -04:00
Crowdin Bot 336695dd55 New Crowdin translations by GitHub Action 2026-06-04 19:03:17 +00:00
Vitor PamplonaandGitHub 38724cc3f5 Merge pull request #3133 from vitorpamplona/claude/wizardly-fermat-y7NBf
Move authenticated identity from policy to connection scope
2026-06-04 15:00:58 -04:00
Claude 7c95ba1ffd refactor: rename removal methods to match what they do
Three names didn't describe their behavior:

- removeFromCache → unlinkAndRemove: the method's main job is unlinking the
  note from every referrer (parents, channels, the report/card/status/poll
  indexes), not just evicting it from the map; the old name only captured
  the last step.
- removeAllChildNotes → clearChildLinks: it clears only THIS note's forward
  child collections and returns them — it does not touch the children's
  replyTo and does not remove anything from the cache. The old name sounded
  more aggressive than detachFromChildren(), which is actually the
  both-directions op.
- Note.removeOnchainZap(source) → removeOnchainZapBySource(source): too easy
  to confuse with removeOnchainZapForSource(txid, pubkey), which is the
  verification-verdict removal with anti-spoof guards. The new name matches
  its inner helper (innerRemoveOnchainZapBySource) and disambiguates the two.

Pure rename: no behavior change. Test names/comments updated to match.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 18:59:54 +00:00
Claude 4ce5f6b1a3 feat(dm): per-relay reach markers for NIP-17, like NIP-04
The gift-wrap history loader exposed only relayCount + reachedBack, so the
conversation's in-stream marker trail showed NIP-04 relays paging back but
never NIP-17 ones. Give AccountGiftWrapsHistoryEoseManager the same
relayProgress map (per-relay reachedUntil / done / stalled) the
per-conversation NIP-04 loader publishes, refreshed on every page, stall,
CLOSE, and cannot-connect.

Move the shared RelayPagingProgress data class out of the NIP-04 UI-package
assembler into service/relayClient/eoseManagers so the gift-wrap manager
can produce it without the service layer depending on a UI package.

ChatroomView now merges both protocols' progress into the gap markers, each
contributing only while it is still paging; a relay that serves both (the
DM inbox relays) collapses to one marker. Markers hide once both protocols
are exhausted.
2026-06-04 18:59:03 +00:00
Claude 9f0ecd549f refactor(dm): page rooms-list NIP-04 history per relay; drop dead pager code
Convert ChatroomListNip04HistorySubAssembler from the round-based model to
the per-relay-independent one, matching the per-conversation NIP-04 loader
and the gift-wrap history loader. A single loadMore opens one window
spanning the whole walk; each relay continues itself on its own non-empty
EOSE (beginRound([relay]) + invalidateFilters), and silent/unreachable
relays are marked stalled (kept open) with exhaustion coming from the
WindowLoadTracker's silence + connect-grace backstops (tracksReqSends=true).
The rooms list now pages both protocols identically.

This was the last user of the round-tally + give-up machinery, so remove it
from UntilLimitPager: roundEventCount(), onClosed()/giveUp() and the
givenUp/closedStreak cursor state + GIVE_UP_AFTER_CLOSES. activeRelays now
excludes only done relays. Delete UntilLimitPagerGiveUpTest (tested the
removed give-up path; abandonment is now the WindowLoadTracker's job, which
its own test covers).

Also drop the now-dead loadEverything/autoLoadAll and the no-progress retry
(no callers; with independent paging one loadMore already walks each relay
to its bottom, and the tracker's backstops cover cold starts).
2026-06-04 18:55:44 +00:00
Claude 9497739c7e docs(quartz/relay): remove auth-scope component diagram 2026-06-04 18:48:26 +00:00
Claude 60b8629a27 refactor(dm): page NIP-17 gift-wrap history per relay, like NIP-04
The gift-wrap history loader was round-based: loadMore asked every active
relay together and the next page only went out after the whole round
settled, so one slow or auth-walled relay throttled the cadence and fast
relays idled until the laggards finished. The per-conversation NIP-04
loader already pages each relay independently; this brings NIP-17 to the
same model.

Now a single loadMore opens one window spanning the whole walk, and each
relay continues itself the instant it EOSEs a non-empty page
(pager.beginRound([relay]) + invalidateFilters, which the sub layer diffs
so only the advanced relay re-REQs). Fast relays race to the bottom while
slow ones catch up in the background. The WindowLoadTracker switches to
tracksReqSends=true with an onAbandoned handler that marks silent/
unreachable relays stalled (kept open, still trying) rather than giving up
on them - exhaustion comes from the window settling via the tracker's
silence + connect-grace backstops, which also removes the need for the old
no-progress retry loop.

Drop the now-dead loadEverything/autoLoadAll (no callers; with independent
paging one loadMore already walks each relay to its bottom). Pin the
history floor per window to keep un-advanced relays' filters stable across
the per-EOSE invalidateFilters, and keep reachedBack monotonic over all
relays.
2026-06-04 18:36:07 +00:00
Claude a3bb6fbc10 refactor: unify Note removal into one shared unlink path
deleteNote() (NIP-09) and removeFromCache() (prune) had drifted into two
near-duplicate "detach a note from the cache" routines. Removal really has
two halves — (1) unlink the note from everything that points AT it, and
(2) handle the note's OWN children — and only the second half differs
between the paths. removeFromCache() already implemented half (1)
completely, so deleteNote() now delegates to it and keeps only its two
delete-specific responsibilities: tearing down gift-wrap hosts and
severing (but keeping) its children via detachFromChildren().

This also fixes a real leak the duplication was hiding. computeReplyTo()
has no ReportEvent branch, so a report note's replyTo is empty and the
report→target link lives only in the explicit reported* index handling.
The old deleteNote() only undid reportedAuthor(), so deleting an
event-level report (reportedPost / reportedAddresses) left the reported
note's `.reports` map holding the removed report note — a partial deletion
that leaked the shell and risked a duplicate Note for the same id.
Delegating to removeFromCache() (author + post + addresses, all
idempotent) closes that gap.

Net behavior change is the report-leak fix only; the redundant
TorrentCommentEvent case is dropped because the torrent target is already
in replyTo (and removed via removeNote), and its @Suppress("DEPRECATION")
goes with it. Adds KDoc to both methods documenting the two-halves model.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 18:35:56 +00:00
Claude d2e64a0339 docs(quartz/relay): update component diagram to the merged design
Reframe from a forward-looking plan diff to the current architecture, and fix
the onAuthenticated signature to (event): Boolean (pubKey was dropped). All
structural elements already matched the merged code.
2026-06-04 18:29:52 +00:00
Claude 89f55a2509 feat(chats): show relay count and reach-back on the reply loader
The unloaded-reply loader matched the history card's chrome but dropped
its status line. Surface the same detail: which protocol, how many relays
it's still asking, and how far back it has paged - reusing the card's
historySubtitle so the inline loader and the oldest-end card read
identically.
2026-06-04 17:42:52 +00:00
Claude 0791ae9d40 test(quartz/relay): assert authenticatedUsers is scoped per connection
The auth set is already a per-RelaySession instance field (one session per
connect(), fresh policy per connection), so two connections never share auth
state. Add a regression test: authenticate different pubkeys on two connections
of the same server and assert each scope holds only its own — no union leak.
2026-06-04 17:42:48 +00:00
Claude 23ddeba8ec fix: sever child back-references when deleting a Note (NIP-09)
deleteNote() removed the target from its parents, gatherers, and the cache
map, but never cleared its own child collections nor dropped itself from
its children's replyTo. That left a partial deletion: every child kept the
removed shell alive through replyTo (a leak), and a reply resolved later
via computeReplyTo would getOrCreateNote a *second* Note for the same id —
breaking the one-Note-per-id invariant.

Adds Note.detachFromChildren(), which clears the note's forward child
collections (via removeAllChildNotes) and severs this note from each
child's replyTo (keeping any other parents). deleteNote() now calls it
before notes.remove(), so once the note leaves the map nothing points at
the dead shell. Orphaned replies become roots, which is correct once their
parent is hard-deleted from the cache.

Adds detachFromChildren coverage to NotePruningReferenceTest.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 17:42:29 +00:00
Claude 6fb735018c refactor(quartz/relay): drop redundant pubKey param from onAuthenticated/authorize
pubKey was always event.pubKey (the NIP-42 signer is the authenticated
identity), so the parameter was pure redundancy. onAuthenticated(event) and
authorize(event) now read event.pubKey directly; the engine still commits
cmd.event.pubKey. Removes the now-unused HexKey imports from IRelayPolicy and
PolicyStack.
2026-06-04 17:32:24 +00:00
Claude a54d9c92a7 refactor(quartz/relay): move auth identity from policy into connection scope
Authentication state (who is logged in on a connection) is connection scope,
not a policy decision. It was stored on FullAuthPolicy, which forced the
AuthScopedPolicy marker, the PolicyStack union, and a downcast in
RequestContext just to route it back out as scope.

Now the engine owns it: RelaySession holds the authenticatedUsers set behind
the (now public) requestContext; the data plane and policies read it through
RequestContext. The policy stays pure decision —
- onConnect(scope, send): a per-connection policy captures the read-only scope
  to gate on; shared singletons ignore it.
- onAuthenticated(): Boolean: the policy's vote on whether to record the
  verified pubkey (default false, so blind-accept policies never record an
  unverified identity). FullAuthPolicy runs authorize() then votes true.
- RelaySession.handleAuth performs the single, engine-side commit after the
  whole chain approves and a verifying policy votes to record.

FullAuthPolicy keeps all auth logic (challenge, accept(AuthCmd), gating,
authorize) and gains a protected authenticatedUsers accessor over the scope for
subclasses (restricted content / filter rewrite). Deletes AuthScopedPolicy,
PolicyStack.authenticatedUsers, and the RequestContext downcast.
2026-06-04 16:45:03 +00:00
Claude c4bfdc72dc fix: detach channel in removeFromCache to mirror deleteNote
removeFromCache() relied solely on note.inGatherers to detach a pruned
note from its channels, while deleteNote() additionally resolved the
channel via getAnyChannel() and removed the note there too. inGatherers
is normally authoritative (Channel.addNote always calls addGatherer), so
this is defensive rather than a confirmed live leak — but it closes the
divergence so both removal paths detach channels identically. Guards
against any future consume path that adds a note to a getAnyChannel-
resolvable channel without the gatherer link: otherwise the note would
linger in the channel's notes map after leaving the cache, leaking it and
letting a relay echo mint a duplicate Note with the same id.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 16:17:14 +00:00
davotoula 21f722767d refactor: use lambda Log overload for interpolated log calls 2026-06-04 18:15:17 +02:00