Commit Graph
16669 Commits
Author SHA1 Message Date
davotoula dca345212b build: aggregate Kover coverage for SonarQube import
- Apply Kover (Apache-2.0, build-time only) to the KMP/JVM modules and aggregate at the root
- include amethyst playDebug in the aggregated Kover coverage
2026-07-11 15:37:40 +01:00
davotoula b8570797fe refactor: extract no-reachability-cache flag name into a constant 2026-07-11 11:44:21 +01:00
davotoula cf95b07855 fix: document intentionally empty default no-op bodies 2026-07-11 11:12:20 +01:00
Vitor PamplonaandGitHub b64c378a9d Merge pull request #3519 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-10 23:20:26 -04:00
Vitor PamplonaandGitHub 5c8801082a Merge pull request #3513 from vitorpamplona/claude/amy-graperank-crawl-perf-jhmx0x
GrapeRank crawl: connect once, wait once — 38% faster at hop-3; hop-8 31% faster at +15% lists / +47% users
2026-07-10 23:19:58 -04:00
Vitor PamplonaandGitHub 4cc88cc0ae Merge pull request #3529 from vitorpamplona/claude/powdisplay-notecompose-sizing-x8ua7i
Reduce PoW badge size for better UI balance
2026-07-10 23:19:34 -04:00
vitorpamplonaandgithub-actions[bot] 835f1e5e85 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-11 03:16:54 +00:00
Vitor PamplonaandGitHub d6c046249e Merge pull request #3528 from vitorpamplona/claude/fix-signed-events-reference-vhrzsa
Mark DM rooms as read when wraps are mined in background
2026-07-10 23:14:35 -04:00
Claude f3084fcf79 docs(graperank): record communal-sweep hop-8 validation (160.7 min, +15.6% lists, +47% users)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zEYRGKF943RgLaHTViJaB
2026-07-11 03:14:15 +00:00
Claude 9801f03ce4 fix: make the PoW pill in note headers a bit smaller
Shrinks the DisplayPoW icon and text from 12 to 10 (dp/sp) and trims
horizontal padding so the pill takes less space inline in NoteCompose
headers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbpEL8rRBwLd2mgbAqBPvX
2026-07-11 03:06:24 +00:00
Claude a3cdb06404 fix: resolve bad merge of broadcastPrivately overloads and DM read marker
The PoW publishing-queue branch split broadcastPrivately into a
NIP17Factory.Result overload delegating to a List<GiftWrapEvent> one,
while the DM-unread branch appended markDmRoomAsRead(signedEvents.msg)
to the old single body. The merge left that call inside the wraps
overload, where signedEvents does not exist, breaking compilation.

Move the read-marker call to the Result overload, and mark the room as
read on the PoW early-return path too (the rumor is signed inline there
and the wraps only publish after mining), so PoW-enabled accounts keep
the unread-clearing behavior from #1286/#1287.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1hu9zpyiuJo6yyoJs1vdw
2026-07-11 03:03:30 +00:00
Vitor PamplonaandGitHub c889952e58 Merge pull request #3526 from vitorpamplona/claude/relay-nip29-groups-button-gadubu
Add NIP-29 relay groups button to relay information screen
2026-07-10 21:48:20 -04:00
Vitor PamplonaandGitHub e60003063b Merge pull request #3527 from vitorpamplona/claude/nip13-pow-publishing-queue-1takp1
Add NIP-13 Proof-of-Work mining queue with persistence and UI
2026-07-10 21:48:03 -04:00
Claude c84cee8107 feat(pow): predictable mining progress — estimated-time bars and time-left labels
The mining banner's indeterminate bar becomes determinate: each mining job
gets its own bar that fills over the device's estimated duration for its
difficulty (elapsed / (2^bits / benchmarked hash rate)), with an
"≈ 10 minutes left" label next to the elapsed time. The nonce search is
memoryless, so past the mean the bar honestly falls back to the
indeterminate sweep with "any moment now" instead of parking at 100%.
Queued-only banners keep the shared activity sweep.

The mining notification mirrors this: single-job progress fills toward the
estimate with the time-left text, refreshed every 30 s while mining (the
queue only emits on state changes, not clock ticks).

The composer difficulty menu now prices each option on this device —
"24 bits · ≈ 45 seconds" — using the same cached benchmark, and the duration
formatting is shared (formatApproxDuration/formatTimeLeft) with the settings
estimate instead of being private to the settings screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-11 01:43:06 +00:00
Claude 195538e25f style: reduce relay header button row padding to 20dp
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012w9KridAbRpesbX2ytdjn4
2026-07-11 01:40:41 +00:00
Claude 39ffb0ed4d feat(pow): move composer PoW button before Subject; switch icon to Manufacturing
The per-post PoW button on the New Post screen now sits before the
Subject/Title toggle instead of last in the options row.

Every PoW surface (composer button, note chip, mining banner, settings tile,
relay-info minimum-PoW row) switches from the Bolt icon to Manufacturing
(gear, U+E726): Bolt is the app's zap/lightning glyph everywhere else, so a
Bolt PoW badge next to the zap-split/zapraiser/invoice buttons read as a
payment feature. Subset font regenerated for the new codepoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-11 01:33:54 +00:00
Claude f17d3e942a feat: add Groups button to relay info screen for NIP-29 relays
Shows a button in the relay information header that opens the relay's
NIP-29 group list (RelayGroupServer route) when the relay's NIP-11
document advertises support for NIP-29. Also converts the header button
row to a FlowRow and marks button labels single-line so labels no
longer break mid-word (e.g. "Member/s") when the row runs out of
width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012w9KridAbRpesbX2ytdjn4
2026-07-11 01:24:31 +00:00
Claude 80ece7c40f refactor(nip59): replace wrap PoW params with a template-conversion hook
GiftWrapEvent.create no longer takes powDifficulty/powIsActive — it takes a
single templateConversion hook ((template, ephemeralPubKey) -> template,
default identity) that runs on the finished wrap template right before the
ephemeral key signs it. The hook receives the ephemeral pubkey because the
NIP-01 id a nonce commits to includes it and the key never leaves create().

NIP17Factory forwards the same hook through wrapSeal/createWraps and the
create*NIP17 entry points, so quartz's NIP-59/NIP-17 code no longer imports
the NIP-13 miner at all; Account builds the mining closure at the call site.
Any future pre-sign wrap adjustment flows through the same seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-11 01:17:18 +00:00
Vitor PamplonaandGitHub 0eb2beda22 Merge pull request #3525 from vitorpamplona/claude/dm-unread-own-messages-jl18pt
Fix DM read state: mark rooms as read when sending messages
2026-07-10 20:55:14 -04:00
Claude 6dd1e4d31d fix(pow): resolve audit findings — durability, coverage, perf and l10n
Durability & correctness:
- Keep the job entry + disk checkpoint alive until the publish continuation
  completes (was: dropped at mining-complete); failed publishes keep their
  checkpoint for restart retry and surface through a failures SharedFlow
- Move draft deletion into the publish continuations in every composer so a
  cancelled or process-killed mining job can't destroy the only copy of a post
- Persist gift-wrap mining: split NIP17Factory into createSeals (signer
  interaction, runs inline) + wrapSeal (pure CPU, runs on the queue), new
  REPLAY_WRAPS records restore pending DM wraps after process death
- Clamp synced NIP-78 difficulty (PoWPolicy.MAX_DIFFICULTY), require a sane
  target in PoWMiner, bound PoWRankEvaluator against short ids
- Reaction double-tap while mining now toggles (dedupeKey + cancelByKey)
  instead of publishing duplicates
- Restore checkpoints for every loaded account, not just the active one
- logOff purges the account's checkpoints and cancels its queued jobs
- Private notes: composer chip now gates on the gift-wrap kind and the
  per-post override reaches sendPrivateNote

Coverage:
- Public/live chat (kinds 42 + 1311) and voice replies (1244 + kind-1 audio
  replies) now route through the mining gate

Perf:
- FGS start() dedupes with a running flag; PendingIntents built once
- Banner 1 Hz clock only ticks while a job shows elapsed time
- PoWEstimator benchmark is single-flight behind a Mutex

UX / l10n:
- Post-mining failures toast with retry information
- Count strings converted to <plurals>; elapsed time via DateUtils; settings
  estimate uses localized units; shared powKindLabelRes replaces three
  duplicated kind→label maps; dead pow_chip_* strings removed
- CLI: pow mine lowercases the pubkey before mining (uppercase hex mined an
  id that never matches the signed event) and validates via quartz Hex

New queue tests: checkpoint lifetime, failure reporting, cancelByKey toggle,
per-owner cancellation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-11 00:31:05 +00:00
Claude 139659bbda Merge remote-tracking branch 'origin/main' into claude/dm-unread-own-messages-jl18pt
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
2026-07-11 00:17:37 +00:00
Claude 507de32575 fix: harden own-message-counts-as-read against audit findings
Addresses the issues found reviewing the #1286/#1287 fix:

- Centralize the room read-marker route in privateChatLastReadRoute()
  and the authorship rule in chatMessageMarksRoomAsRead(); all writers
  and readers now share one implementation.
- Exempt notes-to-self rooms from the authorship rule — there the
  user's own messages are the content still to be seen.
- Mark own chat messages as read at the ingestion choke point
  (EventProcessor), covering every send path (including the play-flavor
  App Functions assistant) and own messages arriving from other devices
  at the persisted-marker level, not just visually.
- Advance the send-path marker to the newest known room message, so a
  quick-reply to a skew-ahead peer also clears the indicators.
- Revert the authorship guard on public-chat/ephemeral/marmot rows:
  in multi-party channels 'I posted last' does not imply the earlier
  backlog was seen, and no marker advances there to back the guard.
- Restore the new-items bubble for rooms whose newest item is an
  unsent draft — a draft still needs the user's attention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqN8cfqNdo1MAvLN4C9krm
2026-07-10 23:51:55 +00:00
Vitor PamplonaandGitHub 57aaf69073 Merge pull request #3524 from vitorpamplona/claude/relay-icons-display-timing-7c3n7c
Fix relay attribution in gift-wrap chains and Marmot messages
2026-07-10 19:11:47 -04:00
Claude f0f57a93ff fix: record relay attribution for repost and community-approval notes
Non-chat audit follow-up: RepostEvent, GenericRepostEvent, and
CommunityPostApprovalEvent consumes never called note.addRelay, so those
notes carried an empty relay list forever.

- CommunityPostApprovalEvent is badge-rendered directly in community feeds
  (BadgeBox has no repost-style indirection for it), so its relay icon row
  was always empty.
- Repost badges borrow the boosted note's relays in BadgeBox, but the
  repost note's own list feeds relayHintUrl()/relayUrls() for nevent hints
  when citing or sharing the repost — those fell back to author outbox
  guesses.

Add the standard attribution block (author.addRelayBeingUsed +
note.addRelay) before the duplicate check, matching consumeRegularEvent,
so both first arrivals and re-deliveries from other relays are recorded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
2026-07-10 23:10:03 +00:00
Vitor PamplonaandGitHub eaf29a265d Merge pull request #3523 from vitorpamplona/claude/markdown-hashtag-fragments-9ya187
Support markdown hashtag links (#tag fragments)
2026-07-10 19:04:02 -04:00
Claude f151cb74fc fix: drill relay attribution into Marmot kind-445 inner notes
Audit follow-up to the gift-wrap relay-icon fix: Marmot group chat rows
render the decrypted inner note (kind 9/7) via the standard chat feed with
RelayBadgesHorizontal, but nothing ever populated that note's relay list —
OK acceptances and relay deliveries all landed on the kind-445 envelope,
which has no link to its inner event.

- GroupEvent now implements HasInnerEvent (same @Transient @Volatile
  innerEventId pattern as GiftWrapEvent/SealedRumorEvent), so
  LocalCache.addRelayToNoteAndInners drills 445 -> inner for both OK
  confirmations and duplicate EVENT deliveries. RouteMaker is unaffected:
  it gates on the concrete wrap types before casting to HasInnerEvent.

- GroupEventHandler sets innerEventId at decrypt time and copies the
  envelope's accumulated relays down to the inner note (looked up by
  event.id, not the eventNote/publicNote params, which belong to the
  triggering event when replayed from retryPendingFor).

- sendMarmotGroupMessage sets innerEventId on the freshly built envelope
  before consuming/publishing, so acceptances for sent group messages
  reach the rendered note as soon as the inner note is indexed.

Also audited the remaining chat-rendered types: NIP-04 PrivateDmEvent,
ChatMessageEvent rumors, NIP-C7 ChatEvent, ChannelMessage/Ephemeral/
LiveActivities messages all route through consumeRegularEvent (duplicates
covered by the shared helper), and NIP-37 drafts render the wrap note
itself with markAsSeen covering the version/addressable pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
2026-07-10 23:01:45 +00:00
Claude 962146bd60 perf(graperank): round-wide communal sweep replaces per-batch backbone re-fan
Retry rounds were the dominant tail cost of deep crawls (rounds 8-10 of a
hop-8 run burned ~80 min for +6.6k lists): every 256-user batch with
attempt>0 or no-outbox users fanned the SAME ~40 backbone+fallback relays,
queueing thousands of small REQs against their 16-permit gates — pure
hot-relay head-of-line blocking.

Restructured:
- routeByOutbox now routes ONLY own-signal relays (write relays; hints when
  no outbox) and flags no-outbox/retry users into a communal set.
- New Phase C communalSweep asks the shared relays ONCE per round for all
  flagged users in author-chunked units — same (relay, user) coverage,
  ~8x fewer REQs — and records answered-empty pairs so each round shrinks.
- Attempt accounting moves to Phase C, after the round's full coverage
  (own relays + shared set) has run.
- shardedSweep now also records answered-empty (user, relay) pairs, so the
  communal sweep never re-asks a pair the shard pass already proved empty.
- Fixpoint recovery dedup: recoverStragglersFromAggregators skips users a
  prior pass already asked the aggregators for (each repeat pass cost ~4 min
  of store scans + REQs at 470k stragglers for zero new answers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zEYRGKF943RgLaHTViJaB
2026-07-10 22:50:08 +00:00
Vitor PamplonaandGitHub 84d16348df Merge pull request #3520 from vitorpamplona/claude/auth-permission-architecture-weyn9x
NIP-42 relay auth: contextual "why", per-situation trust, and prompts
2026-07-10 18:48:54 -04:00
Claude 81dc4449bd feat(cli): NIP-13 for amy — post --pow and the pow verb group
- `amy notes post TEXT --pow BITS [--pow-timeout SECS]` mines the note
  pre-signature via quartz's PoWMiner (blocking — the CLI process is the
  job), exits 124 on timeout with nothing published, and adds additive
  --json keys: pow, pow_target, pow_millis.
- `amy pow check EVENT-JSON|-` reports actual_bits, committed_target,
  has_commitment and effective_pow (capped at the commitment per
  NIP-13's anti-lucky-spam rule) plus id+sig validity.
- `amy pow mine --target N [--pubkey HEX] [--timeout SECS] TEMPLATE|-`
  mines an unsigned template for any pubkey — NIP-13's delegated PoW:
  ids don't commit to signatures, so a headless box can mine for a
  phone and hand the template back for signing.
- `amy pow bench` prints the machine's hash rate and expected seconds
  at 16/20/24/28 bits (commons PoWEstimator).
- cli/tests/pow/pow-headless.sh: relay-free harness covering bench,
  mine (commitment shape + 124 timeout), and the delegated round trip
  (mine → sign via `amy event` → pow check ≥ target). 6/6 passing.

No logic added to cli/ — thin assembly over quartz nip13Pow + commons
PoWEstimator per the CLI architecture rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-10 22:47:05 +00:00
Claude 8d528ea01c revert(relayauth): drop the "couldn't deliver your event" toast
The give-up toast fired per-relay, but Nostr publishes each event to several
relays (NIP-65 outbox), so one relay rejecting an event that reached the others
produced a misleading "couldn't deliver" popup. It also named no event and fired
mid-scroll on reconnect-driven re-pumps, so it read as random noise. Remove the
toast and its subscription/strings.

The low-level quartz onEventGaveUp signal stays (tested, no UI consumer) as a
primitive for a future per-message send-status indicator, which is the right
surface for delivery failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:45:47 +00:00
Claude d1ba04ad52 fix(relayauth): pre-merge audit — auth retry budget + lost-prompt window
Two bugs found in a pre-merge audit:

- quartz PoolEventOutboxState: auth-required NAKs only spared the `responses`
  budget, but `tries` (grown by every send/re-pump and NOT auth-aware) still
  accumulated across reconnects, so a slow/flapping AUTH handshake could exhaust
  Tries.isDone() and drop the event — with a spurious onEventGaveUp — before AUTH
  landed. Now an auth-required NAK resets the relay's retry budget (it responded,
  so it's up and just wants auth). Regression test added.

- RelayAuthPromptBus used a replay=0 SharedFlow, so a challenge that resolved to
  ASK before RelayAuthPromptHost subscribed (cold start / account switch) was
  dropped and the auth coroutine stalled the full timeout then DISMISSed. Add
  replay so late subscribers recover pending prompts (the host already filters
  resolved ones). Regression test added.

Also record the as-built design (Always/Never/Custom + toggles, venues, give-up
toast, known deny-relay-outbox limitation) in the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 50d942cfce style(relayauth): use the settings design system for the toggle and relay blocks
Adopt the same componentized UI as the Security Filters screen for blocks 2 and
3 (block 1, the Always/Never/Custom mode selector, stays as the policy cards):

- The Custom toggles are now a SettingsSection card of SettingsSwitchTiles
  (leading colored icon box, title + description, inset dividers) — reusing the
  shared settings components instead of bespoke rows.
- The per-relay list is now a grouped rounded card (surfaceContainerLow) with
  the same header style. Kept lazy: each row is its own LazyColumn item that
  clips the card's top/bottom corners on the first/last row so contiguous rows
  read as one card, preserving the earlier perf fix.
- Section headers unified to the SettingsSection primary-colored style.

Toggle icons: Dns (my relays), Download (read follows), Mail (message follows),
Public (message strangers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 0f8b276af7 perf(relayauth): lazily render the per-relay list; fix trailing divider
The per-relay list was a non-lazy Column + forEach, so every relay row composed
on first frame — each building a NIP-11 relay icon (robohash generation is
CPU-heavy) plus up to three avatars. With many authenticated relays that is a
lot of synchronous main-thread work, making the screen slow to open. Move the
whole screen to a LazyColumn so only the visible rows (and their icon/avatar
loads) compose.

Also render the per-relay divider only between rows (keyed itemsIndexed, skip
index 0) so there's no full-width divider trailing after the last row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude a16a9f4522 style(relayauth): match the settings-screen house layout
Drop the ad-hoc cards/thin-divider layout and adopt the same structure as the
other settings screens: small primary-colored SectionHeaders, 4dp dividers
between sections, and plain padded rows instead of surfaceVariant blocks.

- The Custom toggles are now standard settings switch rows (24dp inset, 16sp
  title / 13sp description) under a "What to log in to" header, not a card.
- The per-relay list is now icon-led rows separated by dividers (like the app's
  other relay lists) instead of chunky cards; tapping a row still opens the
  relay's NIP-11 info.
- Section labels reworded: "When to authenticate" and "What to log in to".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 3a96554c8d feat(relayauth): replace the policy list with Always / Never / Custom + toggles
Restructure the global auth control into a top-level mode — Always authenticate,
Never authenticate, or Custom — where Custom reveals independent per-situation
toggles instead of the confusing single sub-toggle:

- My relays and venues (own relays + joined/subscribed/favorited venues) — on
- Read posts from people I follow — on
- Message people I follow (DMs, replies, notifications) — on
- Message anyone / strangers — off by default (you're asked each time instead)

RelayAuthPolicy is now {ALWAYS, NEVER, CUSTOM}. The resolver takes a
RelayAuthCustomToggles plus split serves-facts (followed-read, followed-write,
stranger-write, own-relay, venue) and, under CUSTOM, allows if any enabled
category matches — else falls through to a prompt. There is deliberately no
"read strangers' posts" category, so that always prompts.

Account settings replace the single delivery flag with four persisted booleans
(default policy CUSTOM; no migration, unreleased). The contextual DM/notification
prompt button now switches to CUSTOM and enables both message toggles. Resolver
tests rewritten per-toggle, including that reading a stranger is never
auto-allowed. Old IF_IN_MY_LIST / TRUSTED_FOLLOWS policies and their strings are
removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 9ca1ba2f5c feat(relayauth): reframe follows trust as relationship vs. message delivery
Restructure the two TRUSTED_FOLLOWS controls so they read as independent ideas
instead of a confusing read/write sub-toggle:

- The "My relays and people I follow" policy now trusts a followed user as any
  counterparty — reading their posts AND reaching them (DM/notification) — plus
  your own relays and joined venues. Reading your follows is no longer gated.
- The sub-toggle is repurposed to "Also log in to deliver my messages": trust a
  relay to send DMs, replies or notifications to anyone you're talking to, even
  people you don't follow.

Resolver: replace servesFollowed{Write,Read}Counterparty with a single
servesFollowedCounterparty, add servesWriteCounterparty (an inbox of anyone
you're messaging), and gate the latter behind the new
messageDeliveryTrustEnabled input. Rename the account setting
relayAuthTrustFollowsForReads -> relayAuthTrustMessageDelivery (+ pref key;
no migration needed, unreleased). The contextual prompt button moves from
read-post prompts to DM/notification prompts and now enables message delivery.
Resolver tests updated for the new inputs, including that the delivery toggle
is write-only and never auto-allows reading a stranger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 3e6cda8407 feat(relayauth): modernize the per-relay list to match the app's relay rows
Restore the "Per-relay overrides" heading and rebuild each relay card using the
same conventions as the other relay screens: the relay's NIP-11 icon (robohash
fallback), the shortened displayUrl instead of the raw URL, last-used as a
subtitle under the name, and the whole card tappable to open the relay's
NIP-11 info screen (Route.RelayInfo). The Allow/Deny chip and Forget stay on
the trailing edge; the served-people facepile sits on its own row below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 7c58ea31fd feat(relayauth): merge override + rationale into one per-relay list
Fold the separate "Why you're logged into these relays" section into the
per-relay list so each relay is one card: URL, an Allow/Deny chip, a facepile
of the people it serves (3 avatars + "+N"), and when it was last used.

Collapse the two removal actions (the override "X" and the rationale "Forget")
into a single Forget that clears both the stored decision and the recorded
reason, so the relay drops off the list — this removes the earlier ambiguity
about what each button did. The Allow/Deny chip now also shows for relays
allowed by policy (no explicit override), so they can be blocked from here too.

Also clarify the confusing read-trust sub-toggle copy so it reads as the
read-side extension of the write-only follows policy ("On its own, the option
above only logs in to send messages or notifications… turn this on to also log
in when downloading their posts"). Drop the three now-unused strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 44462ecce1 fix(relayauth): reword follow-trust button to "Always allow my follows' relays"
Shorter and clearer than "Always allow relays from my follows".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude e9dad44131 fix(relayauth): shorten the follow-trust button label
"Always allow relays of people I follow" overflowed the button; shorten to
"Always allow relays from my follows".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 620eeadc2b feat(relayauth): one-tap "trust my follows' relays" on read-post prompts
On a "Load posts from <user> and others" (READ_OUTBOX) auth prompt, add a
button that turns on the broad rule instead of allowing one relay at a time:
it sets the policy to TRUSTED_FOLLOWS and enables the read-trust sub-toggle,
so every relay serving people the user follows auto-authenticates for reads
and these prompts stop appearing. Read-trust only applies under
TRUSTED_FOLLOWS, so the button sets both to keep its promise on any prior
policy, then authenticates the current connection once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 672fb931c8 test: integration-test NIP-17 DM delivery through an auth-required relay
Drive a real NostrClient against an in-process geode relay running
FullAuthPolicy, publishing a NIP-17 gift wrap through the PoolEventOutbox
retry queue. The first EVENT races ahead of AUTH and is rejected
`auth-required`; a RelayAuthenticator answers the challenge and the
still-pending wrap is resent on the post-AUTH resync and stored. This is
the integration counterpart to PoolEventOutboxAuthTest and exercises the
"auth-required must not burn the retry budget" fix end-to-end. A control
test (no authenticator) proves the relay genuinely gates, so the delivery
assertion isn't vacuous.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 893b3c7140 test: cover DataStoreRelayAuthPermissionStore round-trips and pruning
Add JVM unit tests (real PreferenceDataStore on a per-test temp dir, no
Robolectric) covering: decision round-trip and hashed-key reverse lookup;
recordUse counterparty merge by kind, the 64-counterparty cap, idempotence,
and that a new counterparty always persists despite the write throttle; and
the reverse-lookup pruning contract — clearRationale/clearDecision prune the
shared url key only when nothing else references the relay, keeping it when a
decision or rationale still remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude e959acb215 refactor: extract testable AuthDecisionResolver from AuthCoordinator
The verdict-folding policy (no-ledgers auto-allow, any-ALLOW short-circuit,
ASK→prompt→remember Allow/Block) was inlined in AuthCoordinator's signing
lambda, untestable without the relay client and signers. Move it to a pure
suspend AuthDecisionResolver.resolve(verdicts, prompt) returning an Outcome
(shouldAuth + optional remembered override), and cover every branch with
unit tests, including that ASK never prompts when an account already allows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude 72f1f72ea7 refactor: share user-load and reason-string helpers across relay-auth UI
The auth prompt dialog (RelayAuthPromptHost) and the relay-auth settings
screen each carried an identical copy of a pubkey→User loader and the
AuthPurposeKind→reason-string mapping. Extract both into
RelayAuthComposeHelpers (LoadRelayAuthUser, relayAuthReasonRes) and point
both call sites at the shared versions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:12 +00:00
Claude a8fe2eee92 refactor: use quartz typed tag parsers in auth purpose derivation
Replace hand-rolled tag-index parsing in RelayAuthPurposeDeriver with
quartz's typed parsers: PTag.parseKey, ATag.parseAddress (filtered by
community/live-activity venue kinds), MarkedETag.parseRoot / ETag.parseId
for channel roots, and Address.parse for filter `a`-tags. Derive the
venue owner pubkey via Address.parse(...).pubKeyHex instead of splitting
the address string by hand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude 95e3a8a54d fix(relayauth): load and observe venue names in the auth prompt
rememberVenueLabel was a non-loading cache snapshot, so a venue not yet
in LocalCache showed its id and never fetched or updated. Bring it to
parity with the person path (checkGetOrCreateUser + observeUserInfo):
get-or-create the public chat / live activity channel and observeChannel
it, which subscribes to relays for the metadata and recomposes when the
title arrives. Communities keep the NIP-72 d-identifier (no fetch needed).

Audit of the rest confirmed correct: person names (rememberDisplayName /
LoadUser / UsernameDisplay) already load + observe; the trust predicates
read .value of Eagerly-shared StateFlows (publicChatList/communityList/
allFollows), which is the right decision-time snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude ffe9637c8d feat(relayauth): resolve real venue names in the auth prompt
The venue prompt said generic "this room"; now it names the actual place.
rememberVenueLabel resolves a public chat channel's title (by event id),
a live activity's title (by address), or a community's d-identifier (its
NIP-72 name), falling back to the address d-tag / short id when the venue
isn't cached. So the prompt reads "Post to nostr-dev?" / "Open <stream
title>?" with a matching consequence ("…your message to nostr-dev won't
be posted."), reusing the same who-slot as the person purposes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude 3526a14ebe feat(relayauth): venue-aware auth for public chats, communities & live streams
The purpose model was person-centric, so an auth-required NIP-28 channel,
NIP-72 community, or NIP-53 live stream broke: reads filter by #e/#a with
no authors, so nothing was attributable -> silent DENY (chat wouldn't
load); top-level posts (no p tags) also silently failed; replies were
mis-attributed as "notify" and trusted on the wrong signal.

Add venue purposes (POST_VENUE / READ_VENUE) carrying the venue id
(channel event-id, or community/live `kind:pubkey:dTag` address). The
deriver recognizes kind-42 channel posts (root `e`), community/live posts
and reads (`#a` 34550/30311), and channel reads (`#e`). Venues are trusted
under TRUSTED_FOLLOWS when you've joined them (publicChatList /
communityList) or their owner — the pubkey in the address, e.g. a live
stream's host — is someone you follow; trusted venues auto-auth for both
reading and posting.

Safety net: any active use we still can't attribute yields an OTHER
purpose so the relay is prompted about instead of failing silently.
Prompt copy gains venue-aware titles/consequences ("Post to this room?",
"If you don't, your message won't be posted."). Default policy already
TRUSTED_FOLLOWS, so joined venues just work. Unit-tested across resolver
and deriver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00
Claude eea072ec42 feat(relayauth): follow-trust default + action-aware prompt copy
User-perspective fixes so the auth prompt stops feeling like "why does
this app keep asking me to log into things":

1. Default policy is now TRUSTED_FOLLOWS (new installs only; a persisted
   choice is untouched). Messaging/notifying people you follow just works;
   only strangers prompt.
2/3. The prompt is reframed around what the user was doing and its
   consequence, not "log in":
   - Title is action-aware: "Send your message to Alice?" / "Notify …?" /
     "Load posts from …?", resolving the counterparty's display name.
   - The message states the real tradeoff (the relay confirms it's you;
     its operator sees which account you are) instead of "log in".
   - A purpose-specific, error-tinted consequence line — "If you don't,
     your message to Alice won't be delivered." — so Block/Dismiss no
     longer silently break the exact thing the user was trying to do.
   Per-purpose reason labels now show only when a challenge spans multiple
   purposes (the title carries the single-purpose case).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:11 +00:00