Commit Graph
16582 Commits
Author SHA1 Message Date
Claude 38b2e65362 feat(quartz): expose pending outbox events per relay for auth context
Add INostrClient.activeOutboxEvents(url) (backed by
PoolEventOutbox.activeOutboxEventsFor) returning the full events still
pending delivery to a relay, not just their ids like activeOutboxCache.
This lets a host explain *why* a relay is being authenticated with —
e.g. a pending kind-1059 gift wrap means we're sending a DM to its
recipient — by inspecting kind/tags. Combined with the existing
activeRequests(url) filters, it is the generic challenge context the
NIP-42 decision hook needs. Updates the INostrClient test fakes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 4bd73596e3 feat(relayauth): add purpose-aware AUTH policy core + TRUSTED_FOLLOWS
Introduce the platform-neutral decision core for contextual NIP-42 auth
in commons/relayauth:
- AuthPurpose / AuthPurposeKind / RelayAuthContext describe *why* a relay
  wants auth (send DM, deliver notification, read outbox, own relay).
- RelayAuthVerdict adds an ASK outcome (runtime-only; never persisted,
  unlike the two-value RelayAuthDecision override).
- RelayAuthResolver is a pure, unit-tested precedence ladder: blocked
  list > per-relay override > policy > ASK-if-attributable-else-DENY.
- New TRUSTED_FOLLOWS policy: auto-auth for relays serving a followed
  counterparty on a write purpose (DMs/notifications), and — behind a
  read sub-toggle — read purposes; strangers fall through to ASK.

Wires the new enum value through the existing settings screen (new
option + strings, reusing the Group symbol) and the URL-only ledger path
(degrades to the my-list check until challenge context is plumbed).
Live prompt UI, grant-rationale persistence, and quartz challenge-context
plumbing are follow-up steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 9c618f9ba1 fix(quartz): don't drop outgoing events on NIP-42 auth-required NAKs
PoolEventOutboxState treated an `auth-required` OK-false the same as any
transient failure, recording it against the per-relay Tries budget
(responses > 2 drops the event on the next send attempt). Relays that
NAK every unauthenticated EVENT could therefore exhaust the budget and
drop the message before the AUTH handshake completed — the event was
gone by the time syncFilters re-sent it after the auth OK.

Treat `auth-required` as a deferred state instead: keep the relay in
relaysRemaining and record no failure, so the existing
syncFilters-after-auth path redelivers it. Mirrors the behavior already
present in StandaloneRelayClient. Terminal rejections (invalid/pow/
replaced/deleted) and ordinary transient errors are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 6c9259fea0 docs: split AUTH permission plan into quartz mechanism vs amethyst policy
Account for the existing (rudimentary) outgoing-event retry in
PoolEventOutbox: syncFilters already re-sends EVENTs after an auth OK,
but auth-required is wrongly treated as a burned retry (hard >2/>3 cap,
no backoff, silent drop) — a race that can drop a message before AUTH
completes. Recommend fixing the resend/retry mechanism generically in
quartz (port StandaloneRelayClient's auth-required handling, add backoff
+ terminal give-up signal, enrich the injected auth-decision hook with
pending-event/active-filter context) and keeping only policy, purpose
derivation, ASK prompt, and grant-rationale UI in amethyst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 3570ebb014 docs: plan for contextual NIP-42 AUTH permissions
Design doc for asking the user *why* an auth is requested (send DM,
deliver notification, read outbox), a follow-based auto-trust policy
mode, an ASK decision state, and per-relay grant rationale shown in the
auth settings screen. Reuses the existing RelayAuthenticator /
AuthCoordinator / RelayAuthPermissionLedger stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:08 +00:00
Vitor PamplonaandGitHub 4d16d6e38f Merge pull request #3522 from vitorpamplona/claude/richtext-trailing-whitespace-85eudl
Strip trailing whitespace from rich text parser output
2026-07-10 18:11:21 -04:00
Claude 24ea909221 fix: remove trailing whitespace and newlines when rendering RichText
Trailing spaces and newlines in a note's content produced empty trailing
paragraphs after the line split, each rendered as a blank FlowRow line
between the last visible word and the end of the component. Trim the
content's tail before segmenting, and return no paragraphs for
whitespace-only content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZET7ig8MvR5vSCSxvcURi
2026-07-10 22:09:11 +00:00
Vitor PamplonaandGitHub 581fded5f5 Merge pull request #3521 from vitorpamplona/claude/brainstorm-world-client-033mhn
Update Brainstorm web app icon URL
2026-07-10 18:06:24 -04:00
Claude 96965305cf fix: use Brainstorm's real apple-touch-icon in browser suggestions
https://brainstorm.world/brainstorm.svg 404s (the SPA serves its HTML
not-found page with a 200), so the Discover row fell back to the globe
glyph. Point at the site's declared 180x180 apple-touch-icon.png instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzQhmuFNULgiP31XqMRpJP
2026-07-10 21:53:49 +00:00
Vitor PamplonaandGitHub 193a5559bd Merge pull request #3516 from vitorpamplona/claude/amethyst-apk-cert-docs-dy2pzo
docs: Add APK signature verification instructions
2026-07-10 16:34:04 -04:00
Vitor PamplonaandGitHub c06c6523b5 Merge pull request #3518 from vitorpamplona/claude/amethyst-trim-post-whitespace-t9xi5m
Trim message text before tagging in post ViewModels
2026-07-10 16:25:54 -04:00
Vitor PamplonaandGitHub 0f89fdb7e1 Merge pull request #3517 from vitorpamplona/claude/amethyst-login-theme-toggle-5af214
Persist login UI state across configuration changes
2026-07-10 16:25:39 -04:00
Claude a509d41358 fix: keep login/signup page choice across uiMode recreation (#1097)
Toggling the system dark/light theme recreates MainActivity (uiMode is
not in configChanges), and the login/signup page choice was held in a
plain remember, so the UI reset from Sign-Up back to Login. Switch it to
rememberSaveable so the choice survives recreation. Same fix for the
password-visibility toggle in LoginScreen.

https://github.com/vitorpamplona/amethyst/issues/1097

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J74bRJ9H1KFx4vTo9wpez3
2026-07-10 20:11:47 +00:00
Claude 03cb5e4123 fix: trim leading/trailing whitespace from composed posts (#349)
Trim the composer text once, where it is handed to NewMessageTagger, so
every event kind built from tagger.message (short note, poll, zap poll,
NIP-29 thread, NIP-22 comment) publishes without stray leading/trailing
whitespace and mention rewriting operates on the exact string that gets
published. The now-redundant per-call trim on the NIP-29 thread build is
removed. Whitespace-only messages are still rejected by the existing
isNotBlank gates before a template is ever built.

https://github.com/vitorpamplona/amethyst/issues/349

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1V6v7aYBE8B5PhNZMNs3o
2026-07-10 20:11:08 +00:00
Claude 412ec16905 docs: publish APK signing certificate SHA-256 for verification (#972)
Adds a 'Verifying the APK signature' section to the README so users
sideloading via Obtainium/AppVerifier/GitHub Releases can check the
signing certificate of their download.

The fingerprint was COMPUTED (not transcribed) from official release
artifacts of the latest release, v1.12.6:

- amethyst-fdroid-arm64-v8a-v1.12.6.apk
  https://github.com/vitorpamplona/amethyst/releases/download/v1.12.6/amethyst-fdroid-arm64-v8a-v1.12.6.apk
  (file SHA-256 ed95fdb39d668ad2100ca6ff90f21617537790c8db47850f36ca36f75bc7aeea,
  matching the GitHub release asset digest)
- amethyst-googleplay-arm64-v8a-v1.12.6.apk
  https://github.com/vitorpamplona/amethyst/releases/download/v1.12.6/amethyst-googleplay-arm64-v8a-v1.12.6.apk
  (file SHA-256 dacc6db1ba14c914504c6629f8fc039de0918a2ea9d35483915b4721391c3b19,
  matching the GitHub release asset digest)

Extracted with 'apksigner verify --print-certs' (build-tools 37.0.0)
and cross-checked with 'keytool -printcert -jarfile'. Both flavors are
signed with the identical certificate (CN=Vitor Pamplona, O=Amethyst
Labs), consistent with create-release.yml signing both flavors with
the same SIGNING_KEY secret:

  SHA-256: c2d0aa86bcb6b62090561a41bbe336e98b78c2d0210a498dc885f28e1348cf17

Fixes #972

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148shLViyE1KjFc69J6xYux
2026-07-10 20:02:35 +00:00
Vitor PamplonaandGitHub 16711caec0 Merge pull request #3515 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-10 13:57:30 -04:00
vitorpamplonaandgithub-actions[bot] 085b4d7dc0 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-10 17:06:25 +00:00
Vitor PamplonaandGitHub 525d3ef189 Merge pull request #3514 from vitorpamplona/claude/armada-nip29-integration-lwqard
Relay Groups: NIP-29 relay-based group chat (Armada interop)
2026-07-10 13:03:48 -04:00
Claude f946e1aaf4 fix: use the Topic icon for the composer's subject toggle
The subject/title toggle used the Article (document-lines) glyph, which reads as
"body text". Switch to Topic — a clearer signifier for a subject/title line.
Codepoint already present in MaterialSymbols, so no font-subset regen needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:50:23 +00:00
Claude 003142122b fix: don't drop NIP-11 waiters on a stuck Loading marker
The relay-signed NIP-29 gate reads each relay's NIP-11 `self` key from the
cache. Nip11CachedRetriever.loadRelayInfo treated a valid `Loading` marker as
"a fetch is already in flight, just wait" — but it dropped the caller's
callback entirely and had no way to notify it when the fetch finished.

That is a lost-wakeup: if the coroutine that started the fetch is cancelled
mid-flight (e.g. the discovery screen that launched the warm-up leaves
composition when you tap a relay chip), the `Loading` marker is left behind,
valid for a full hour. The relay's on-group-list screen then mounts, sees the
stuck `Loading`, waits forever, and never receives the doc — so its NIP-11
looks empty (no `self`), the self-key gate rejects every 39000, and a group
you can plainly see under the "Mine" filter (which bypasses the gate) vanishes
on the relay page.

Re-fetch on `Loading` instead of silently waiting: the fetch is cheap, dedups
at the HTTP layer, and guarantees this caller is notified. Error caching is
unchanged (still avoids hammering a genuinely broken relay).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:27:02 +00:00
Claude af0e9c512e fix: warm host-relay NIP-11 in discovery so All-Follows shows their groups
Companion to the All-Follows roster fix: the discovery screen warmed NIP-11
only for the outbox `candidateRelays`, but a group's relay-signed 39000 lives
on its HOST relay. In All-Follows the subassembler now probes those host
relays (joined kind-10009 + favorited kind-10012) for follow rosters, so their
39000s land in the cache — but the self-key gate (isRelaySignedRelayGroup)
then read an unwarmed, empty NIP-11 (self=null) and dropped every one of them.

That is why the groups only appeared after bouncing through Global (whose
relay set happens to include the host relay, warming it as a side effect).
Warm the joined + favorited host relays alongside the outbox candidates so
the gate is a cache hit with the relay's `self` key present.

Verified against wss://basspistol.org: NIP-11 advertises nip-29 with
self=afd7da3f…, all six 39000s are signed by that self key, and
`amy relaygroup browse` returns all six with unverified_dropped=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:15:28 +00:00
Claude 4d81d298ec fix: load follow-participated relay groups in All-Follows without Global
The All-Follows / Authors discovery filter sets resolve their relays via
the outbox model (each follow's own publish relays), but a NIP-29 roster
(kind-39001 admins / 39002 members) lives ONLY on the group's host relay.
So a follow who is an admin or member of a group never surfaced in
All-Follows until the user bounced through Global — which pulls the whole
directory — and back.

Additionally query the follows as `#p` against the group-host relays we
already know about (joined via kind-10009 + favorited via kind-10012),
minus the relays the outbox filter already covers, and re-assemble when
either list changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 16:08:21 +00:00
Claude babdea67ab feat: list joined groups' relays as filter chips in Relay Groups discovery
You had to Favorite a relay (kind-10012) before it appeared as a chip in the
Relay Groups top-nav filter — even for relays you already have groups on. Now the
host relay of every group in your joined list (kind-10009) also shows up as a
relay chip, so you can browse the other groups on those relays without favoriting
them first.

Added only to the relay-groups discovery catalog (not git/podcasts/etc.), deduped
against relays already present as favorites. Selecting one resolves to that
relay's groups (filtered by the relay-signed self-key check as usual).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 15:51:51 +00:00
Claude fa0883f4a0 perf: warm NIP-29 relays' NIP-11 in parallel + pre-warm joined groups
The relay-signed group check reads each host relay's NIP-11 `self` from cache,
but the discovery screen was warming those docs SERIALLY — Nip11Retriever awaits
each HTTP fetch, so N relays meant N sequential round-trips and one slow or
unreachable relay stalled every group behind it until its socket timeout. That's
why groups trickled in.

- Add WarmNip11(relays): fans the fetches out, one coroutine each, so the wait is
  the slowest single fetch instead of the sum. Discovery now uses it and
  re-invalidates the feed as each doc lands (via a version counter).
- Pre-warm NIP-11 for joined groups' host relays from the Messages tab
  (WarmJoinedRelayGroupNip11), so by the time one surfaces in discovery the
  answer is a cache hit. NIP-11 stays cached for an hour.

Note: the Messages tab itself never needed this — joined ("My Groups") rows are
authoritative from the kind-10009 list and were never gated on NIP-11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 15:43:20 +00:00
Claude 0f01dc0c12 feat: verify NIP-29 group metadata is signed by the relay's own key
NIP-29 metadata/roster events (39000-39003) "are addressable events signed by
the relay keypair directly ... as stated by the NIP-11 `self` pubkey", and
"relays shouldn't accept these events if they're signed by anyone else". So the
authoritative test for a genuine group is `39000.author == relay.self` — which
also rejects a stray user-published 39000 even on a real NIP-29 relay, something
the earlier supported_nips heuristic could not.

Add `isRelaySignedRelayGroup(channel)`: strict `author == self` when the relay
publishes `self`, falling back to `supported_nips ∋ 29` when it omits `self`, and
false when it has neither. Apply it at the surfaces that show unsolicited groups:

- Discovery feed: replace the relay-level supported_nips filter with the
  per-channel self-key check in matches(); the screen now warms each candidate
  relay's NIP-11 and re-invalidates the feed as each doc resolves.
- On-relay group list: filter to relay-signed groups, warming that relay's
  NIP-11 so genuine groups fill in and fakes stay hidden.
- CLI `relaygroup browse`/`info`: fetch the relay's NIP-11 (new
  Context.relayInfo) and drop 39xxx not signed by `self`; browse reports the
  dropped count.

Explicit user actions (a received invite link, opening an naddr) are left
untouched — hiding those would be user-hostile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 15:18:13 +00:00
Claude 188ba3c609 feat: discovery only shows groups from relays that advertise NIP-29
Stray kind-39000 events published by ordinary users to general relays (e.g.
nostr.wine) were surfacing in the Relay Groups discovery feed as joinable groups
that have no roster and no chat and can't actually be joined — because those
relays don't run NIP-29, they just store the fake metadata like any addressable
event.

A relay that truly runs NIP-29 rejects user-authored 39xxx, so on such a relay
every 39000 is relay-signed and genuine. Gate discovery on that: restrict the
per-relay constraint set to relays whose NIP-11 `supported_nips` advertises 29.
This is the single point both the match test and the REQ-driven feed read, so
non-advertising relays drop out wholesale. The discovery screen warms each
candidate relay's NIP-11 and re-invalidates the feed as support resolves (a
relay whose NIP-11 lands after its 39000s would otherwise stay hidden until a
manual refresh). "My Groups" (the kind-10009 joined list) is unaffected.

Trade-off: a relay that runs NIP-29 but doesn't publish NIP-11 (or omits 29 from
its list) is hidden from discovery until it advertises.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 14:32:30 +00:00
Claude 9a5a278b01 feat: block NIP-29 group creation on relays that don't advertise it
The reported "title/image didn't save, group shows as a bare hex, and I'm asked
to join my own group" all come from creating a group on a relay that isn't
running NIP-29: it stores the 9007/9002 as ordinary events but never creates the
group, emits 39000/39001/39002 metadata, or makes the creator an admin.

Gate the create screen on the relay advertising NIP-29 in its NIP-11
`supported_nips`: a tri-state check (checking / unsupported / supported) disables
the Create button until support is confirmed and shows an explanatory warning
banner when the relay is confirmed to lack it. Editing is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 13:13:30 +00:00
Claude 567cc3f416 style: match the composer subject/title field to the new-DM To field
Restyle the ShortNotePostScreen subject/title input to the inline-label +
borderless ThinPaddingTextField + hairline-divider look used by the new-DM
composer's "To"/"Subject" rows, instead of a boxed OutlinedTextField. The field
now backs onto a TextFieldState (like the DM composer) rather than a String.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 00:29:41 +00:00
Claude d650c184de feat: require the title for NIP-29 group threads (no text fallback)
The group-thread composer now treats the subject/title field as mandatory: the
field is always shown (no toggle to hide it), canPost() blocks until it's filled,
and createTemplate uses it verbatim as the kind-11 title — dropping the previous
first-line-of-the-body fallback. Plain kind-1 notes keep the optional subject.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 00:21:25 +00:00
Claude bf288711a2 feat: optional subject/title field in the ShortNotePostScreen composer
Add a subject line toggled from the composer's bottom row. On a kind-1 note it
becomes a NIP-14 `subject` tag; on a NIP-29 kind-11 group thread it becomes the
`title` (superseding the first-line-as-title heuristic — that stays as the
fallback when the field is left empty). The field is auto-shown for group
threads and labelled "Title" there, "Subject" otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 00:14:05 +00:00
Claude cb90362bae feat: warm each group's recent messages on a relay's channel list
Opening a group from the relay's channel list used to start its chat from a cold
load. Mount the existing RelayGroupWarmupSubscription on every visible card
(content-only — the directory subscription already streams metadata), so a tap
lands on already-cached messages.

Add a contentLimit to the warmup (default 50, unchanged for discovery's "50+"
signal); the channel list passes ~10 — a first screen's worth. Bounded to
visible rows by the LazyColumn and released as they scroll off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-10 00:13:53 +00:00
Claude 1ecc97de76 feat: compose NIP-29 group threads in the full ShortNotePostScreen
The group "new thread" FAB opened a cramped title+body screen. Point it at the
rich ShortNotePostScreen instead (attachments, emoji, previews, markdown), and
teach that composer to emit a kind-11 group thread when opened for a group.

- Route.NewShortNote gains groupThreadId + groupThreadRelayUrl; the group Threads
  FAB navigates there.
- ShortNotePostViewModel.setGroupThread arms a group-thread mode: createTemplate
  builds a kind-11 ThreadEvent (title = first line, body = the rest, `h` scope),
  and sendPostSync publishes it ONLY to the group's host relay via
  signAndSendPrivatelyOrBroadcast — bypassing the outbox/private/scheduled paths.
- The poll, private-note and scheduling toggles are hidden in group-thread mode
  (they don't apply / would break the host-relay pin).
- Delete the now-unused RelayGroupNewThreadScreen and its route/nav dispatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 23:35:40 +00:00
Claude 6292ceb803 feat: grouped-by-relay Messages view + a Messages settings screen
Move the NIP-29 inline/by-relay toggle off the top of the Messages tab (where it
sat clipped behind the tab row) into a new Settings › Messages screen, and make
"by relay" actually mean something in the feed.

- New MessagesSettingsScreen (Route + SettingsCatalog entry + nav) with a
  radio choice: show each joined group inline, or collapse each relay's groups
  into one row. Removes the pinned SegmentedButton + above-pager server list
  from both the single- and two-pane layouts; deletes the now-dead
  RelayGroupViewModeToggle and RelayGroupServerList composables.
- GROUPED mode now weaves one row PER HOST RELAY (never duplicated) into the
  Messages feed, positioned at that relay's newest group message so it
  interleaves with DMs by recency and shows the last message. Backed by a
  synthetic RelayGroupServerRoomNote whose createdAt mirrors the newest message;
  ChatroomListKnownFeedFilter builds/updates it in feed(), applyFilter and
  updateListWith, keyed by relay url. A view-mode change forces a feed rebuild.
- Revert the moot kind-7 render guard: the data-layer content filter already
  keeps reactions out of the row, so the ChatroomEntry fallback stays simple
  (the shared Event.isGroupChatContent helper remains, used by the feed filter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 23:17:19 +00:00
Claude d4e71881e8 fix: sign a q quote-tag when replying to a kind-9 group message
The NIP-29 group composer always built replies with ChatEvent.build and ignored
replyTo entirely, so a reply to a kind-9 message quoted its parent in the UI but
the signed event carried no NIP-18 `q` tag (and no `p` notify to the author) —
unlike the public-chat and live-activity paths, which use `.reply(...)`.

Route group replies through ChatEvent.reply when replyTo is set: it emits the
`q` quote tag, and we add a `p` tag to the parent's author.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 23:17:03 +00:00
Claude 7c7608913d fix: never render a group reaction as the Messages row content
The Messages-list feed already filtered kind-9/1068/11/1111 content when
selecting a group's representative note, but the ChatroomEntry render fallback
still rendered ANY group-scoped note — including a kind-7 reaction lingering in
the in-memory list — as the group row, using the reaction's content and time.

Add a shared quartz helper `Event.isGroupChatContent()` and use it in both
places: the feed filter and the render fallback. A non-content group-scoped note
now falls back to the channel placeholder ("No messages yet") instead of showing
the reaction as the room's last message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 22:39:15 +00:00
Claude b36daf80f6 feat: notify on reactions/replies to my messages in joined groups
Notification events were only fetched from my inbox relays (#p=me), but NIP-29
group activity — a reaction or reply to my message — lives on the group's HOST
relay, so it never surfaced in Notifications until I opened the group (which
subscribes to the group's content directly).

Extend the notifications subscription to also poll each joined group's host relay
for events that tag me, scoped by `#h` to my joined groups (new
filterGroupNotificationsToPubkey over reaction/reply/repost/zap/report kinds).
Re-subscribe when the joined-group list changes so a newly-joined group's relay
is added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 22:15:54 +00:00
Vitor PamplonaandGitHub 268a9ff33e Merge pull request #3512 from vitorpamplona/claude/nostr-blocked-relays-review-7m5o3t
Enforce blocked relays centrally via decorator client
2026-07-09 18:12:23 -04:00
Claude 5425b09fef fix: don't treat a group reaction as the group's latest message
filterRelevantRelayGroupMessages picked ANY group-scoped note (isGroupScoped()
= carries the group's `h` tag), so a reaction (kind 7) to my message became the
group's "last message" on the Messages tab — a wrong row that the chat renderer
can't display. Whitelist actual chat content (kind 9 chat / 1068 poll / 11
thread / 1111 comment) and reject reactions, deletions, labels, etc. Apply the
same guard to the feed()'s channel-notes scan as defense in depth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 22:11:44 +00:00
Claude ace7a7f476 fix(relay): enforce blocked relays centrally on every REQ/COUNT/publish
Relay targeting is fully distributed: every feed, loader, finder and
broadcast path builds its own relay set and hands it to the shared
INostrClient. Only the follow-outbox flows and the top-nav feed filters
subtracted the NIP-51 kind:10006 blocked list, so blocked relays still
leaked in through the event/thread loaders (FilterMissingEvents /
FilterMissingAddressables), the user-metadata finder
(pickRelaysToLoadUsers), channel finder, DM targeting, the one-shot
fetch helpers, and the publish path (Account.computeRelayListToBroadcast)
— none of which consulted the blocked set.

Add BlockedRelayFilteringClient, a thin INostrClient decorator that
strips the active account's blocked relays from subscribe, count and
publish right before they reach the pool. Because the one-shot fetch
helpers route through subscribe/count, wrapping the client covers them
too. The blocked set is read per-call so account switches and list
edits apply with nothing to invalidate.

Wire it around the shared app client (blocked set from the logged-in
account) and around the per-account crawl client used by Event Sync and
Cashu discovery. Add commonTest coverage for the filtering, pass-through,
fully-blocked, and per-call-read behaviors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNMPdC2eGUwTrt3XkQefuf
2026-07-09 21:50:55 +00:00
Claude 0366d6d584 fix: render group messages on the Messages tab even when unattached
A NIP-29 group message whose note isn't attached to its RelayGroupChannel (its
gatherer never registered — loaded before the channel existed, or via a path
that skips attach) hit ChatroomEntry's when(event) with no matching case and fell
to `else -> BlankNote()`: a white, non-clickable row where the group should be.
The inGatherers check only covers attached notes.

Handle group-scoped events explicitly before the when: resolve the group from the
event's `h` tag + the note's provenance relay and render RelayGroupRoomCompose,
so the row shows the group and taps through to the chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 21:48:28 +00:00
Claude 2eb7ff2ef2 Merge remote-tracking branch 'origin/main' into claude/armada-nip29-integration-lwqard
# Conflicts:
#	cli/tests/.gitignore
2026-07-09 21:48:04 +00:00
Claude a645688baf test+polish: verify NIP-29 placeholder gatherer; show "No messages yet"
Add a regression test proving RelayGroupChannel.placeholderNote() carries the
channel as a gatherer and is cached/stable — this is what lets the Messages row
renderer resolve the event-less placeholder back to the group (the prior fix).

Also give the empty-group placeholder row a visible "No messages yet" second
line instead of blank content, matching the Marmot-group row, so a just-joined
group with no messages reads clearly rather than looking like an empty item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 21:21:59 +00:00
davotoulaandClaude Fable 5 1e05a086ad fix: suppress RestrictedApi false positive on NappletHostActivity.dispatchKeyEvent
Activity.dispatchKeyEvent is a public framework hook; lint flags the
override only because androidx.core's intermediate override carries a
library-group @RestrictTo. Scoped to the method so the check stays live
for genuine restricted-API use. Makes :nappletHost:lintDebug pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLfwhTdf72qFPnmPRYnzqu
2026-07-09 22:14:57 +01:00
Vitor PamplonaandGitHub f1419377b7 Merge pull request #3511 from vitorpamplona/claude/graperank-sync-crawl-1n05im
graperank: relay reachability cache (NIP-66), aggregator kind:3 recovery, and outbox-discovery dedup
2026-07-09 17:00:09 -04:00
Claude 7c581edddc fix: render the NIP-29 empty-group placeholder row instead of a blank
The Messages placeholder guard in ChatroomHeaderCompose only recognized Marmot
placeholders, so a just-joined NIP-29 relay group (an event-less placeholder note
carrying a RelayGroupChannel gatherer) fell through to the wait-for-event branch
and rendered BlankNote() — a white gap where the group row should be. Recognize
a RelayGroupChannel gatherer too so it routes to the group row renderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 20:56:15 +00:00
Claude cc17b29bc0 fix(graperank): stop dropping events, un-evict live-but-slow hosts, re-sweep new relays
Addresses correctness/perf issues found in the crawler + reachability audit:

- deadHosts permanent eviction (#1): an authority that accrued timeoutEvictStrikes
  before its first EOSE was evicted forever — clearTimeoutStrikes only zeroed the
  counter and could not un-evict, contradicting the "a host that ever produces is
  never evicted" invariant. Add a producedHosts set that isDead() consults, so a
  proven-productive authority is never treated as dead even if a concurrent strike
  from the 24-worker fan-out raced it into deadHosts.

- Parking-disabled event loss (#2): when parking is off (no bgScope, or
  parkTimeoutMs <= timeoutMs), a relay that streamed events but didn't EOSE in the
  fast window had its buffer dropped without persist() and reported count 0. Drain,
  persist, and return those events like the other two branches; strike only when
  nothing was delivered.

- Wide-sweep over-narrowing (#4): relayListDiscoverySwept excluded an already-swept
  straggler from the wide pass even though the wide net grows each round, so a 10002
  hosted only on a later-learned relay was never fetched. Gate the wide pass on the
  asked-relay set (wideRelaysSwept) instead: new users get the full net, older
  stragglers get only newly-appeared relays, no (user, relay) pair asked twice.

- Onion detection (#10): replace loose relay.url.contains(".onion") with
  RelayUrlNormalizer.isOnion() in isDead() and networkTypeOf(), fixing the
  foo.onionfake.com false positive and the store/crawler disagreement.

- rtt-open=0 semantics (#9): document that the crawler's reachable records use
  rtt-open purely as a liveness flag (0 = latency not probed), not a real 0 ms
  measurement, and must not be published as authoritative latency data.

deadHosts is deliberately still NOT persisted to the 24h reachability cache (#8):
a timeout eviction means "too slow under our fan-out this run", not "proven
unreachable", so persisting it would blacklist slow-but-live hubs across runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSW59hJtP4Yn8fnRUxc7F5
2026-07-09 20:43:38 +00:00
Claude b935995fe6 fix: open NIP-29 group posts in the group chat, not a thread view
Tapping a NIP-29 message in Notifications (or the feed) fell through routeFor's
`else -> Route.Note`, opening the generic thread view instead of the group chat —
unlike every other chat NIP. Mirror the Marmot-group path: when a note is
attached to a RelayGroupChannel gatherer, route to Route.RelayGroup (the channel
carries the host relay). Add an `h`-tag + provenance-relay fallback for a
group-scoped note that isn't attached to a channel yet, so it still opens the
chat rather than a thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 20:24:39 +00:00
Claude dae4ade55a fix: show a just-joined NIP-29 group on the Messages tab immediately
The Messages tab (INLINE mode) mapped each joined group to its newest cached
kind-9 message and dropped it when none existed. Since the joined-groups
subscription only fetches roster kinds (39000/1/2), not chat, a group you just
joined stayed invisible until you opened it (loading messages) or posted — unlike
Marmot groups, which already fall back to a placeholder row.

Mirror the Marmot pattern for relay groups:
 - RelayGroupChannel.placeholderNote(): a cached synthetic note that adds the
   channel as a gatherer, so the existing Messages row renderer resolves it back
   to the group (RelayGroupRoomCompose already handles a null-event note).
 - ChatroomListKnownFeedFilter.feed(): fall back to placeholderNote() when the
   group has no loaded message.
 - AccountFeedContentStates: rebuild dmKnown when relayGroupList (kind 10009)
   changes — join/leave doesn't flow through newEventBundles, so without this the
   placeholder wouldn't appear until a later event.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 20:22:00 +00:00
Claude 2ffd64c170 refactor: rename relay-group REQ assemblers by when they run
The assembler names didn't say when each is active — most confusingly, a
"Threads" assembler with no matching "chat" one, because group chat (kind-9) is
served by the shared `channel` assembler, not a group-specific one. Rename the
four group-specific families for their surface, and document that chat has no
dedicated assembler:

  relayGroupDirectory  -> relayGroupsOnRelay        (browsing one relay's channels)
  relayGroupRoster     -> relayGroupMyJoinedGroups  (metadata+rosters of joined groups)
  relayGroupThreads    -> relayGroupThreadFeed      (a group's forum-threads tab)
  relayGroupPreview    -> relayGroupWarmup          (prefetch before a group opens)

Each family's FilterAssembler / QueryState / SubAssembler / Subscription + file
renamed to match. relayGroupsDiscovery is left as-is: it already names the
Discover feed and shares its token namespace with the screen/DAL/settings, so a
rename would either collide with RelayGroupDiscoveryFeedFilter or corrupt those.
Pure rename; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 20:05:43 +00:00
Claude aa5c8f0491 revert(relay): drop FrameDispatchStats — per-frame cost on the shared WS hot path
FrameDispatchStats stamped a ValueTimeMark on every relay frame and recorded a
contended atomic per frame in BasicOkHttpWebSocket — the WebSocket layer used by
the whole app, unconditionally, forever — to answer a one-time question that only
graperank --diagnose read. It served its purpose (proved the our-side dispatch
lag is ~200ms mean and the EOSE-wait is dominantly relay-side, so the crawler is
network-bound), but the ongoing per-frame Pair allocation + atomic contention on
every client's relay traffic isn't worth carrying. Revert the channel back to
Channel<String> and delete the stats holder. The diagnose-gated saturation ticker
and per-drain latency breakdown stay — they're crawler-local, off the hot path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSW59hJtP4Yn8fnRUxc7F5
2026-07-09 19:52:42 +00:00