Commit Graph
16699 Commits
Author SHA1 Message Date
Claude cbeff0e8d8 docs(concord): mark chat + control refactor slices done in the plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 04:36:12 +00:00
Claude dc2abc0e1c refactor(concord): control edition (3308) gets its own event package
Second refactor slice (plan: quartz/plans/2026-07-11-concord-event-classes.md).
The kind-3308 control edition now follows the nip88Polls per-kind shape:

- New cord04Roles/control/ package: ControlEditionEvent (the Event subclass +
  build{} template + typed accessors), TagArrayBuilderExt (vsk/eid/ev/ep/vac),
  TagArrayExt (readers), and tags/ classes VskTag/EidTag/EvTag/EpTag/VacTag with
  parse/assemble/isTag.
- ControlEditionBuilder.rumor now delegates to ControlEditionEvent.build +
  RumorAssembler; the raw stringly-typed tag construction and the
  ControlEdition.TAG_* constants are gone. Tags stay in the fixed vsk,eid,ev,ep,vac
  order so chain rumor ids are unchanged.
- ControlEdition.fromRumor reads the typed tags, preserving exact validation
  (present-but-malformed ep/vac still rejected; absent = genesis/owner).
- Register kind 3308 -> ControlEditionEvent in EventFactory so decrypted control
  rumors parse as the typed class.

The edition hash is over content fields (not tags), and all Concord tests —
ControlEditionTest, the fold/authority suite, and the join-flow round-trip — pass,
so the wire format and chain are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 04:27:49 +00:00
Claude c2cbd602bc refactor(concord): chat plane reuses standard events + typed binding tags
First slice of the nip88Polls-structure refactor (plan in quartz/plans/
2026-07-11-concord-event-classes.md). A Concord chat rumor IS a standard Nostr
event plus a channel/epoch binding, so stop re-deriving it by kind number:

- New cord03Channels/tags/ChannelTag + EpochTag (typed, parse/assemble), plus
  TagArrayBuilderExt (channel/epoch/channelBinding) and TagArrayExt
  (concordChannel/concordEpoch/isConcordBoundTo) — the poll-package shape.
- ChannelChat.message/reply now build via ChatEvent.build{ channelBinding(...) }
  and assemble the template into a rumor; reaction reuses ReactionEvent.KIND. The
  minimal e/p/k reaction tags and q/p reply tags are kept byte-identical for Armada
  interop (guarded by the Concord round-trip tests, which pass).
- Drop the ConcordKinds.MESSAGE/COMMENT/REACTION/DELETE aliases (they shadowed
  ChatEvent/CommentEvent/ReactionEvent/DeletionEvent KINDs); callers use the real
  event KINDs. VoicePresence uses the new tag classes instead of bindingTags.

Remaining Concord-specific kinds (control 3308, invites, guestbook, rekey, voice,
seals/wrap) get their own per-kind packages in later slices per the plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 04:05:07 +00:00
Claude f18ba03a48 fix(concord): store the kind-13302 list replaceably so the hub populates
The Concord hub was empty even after the 13302 arrived: ConcordCommunityListEvent
extended plain Event, so LocalCache filed it by id and the dispatch when() had no
branch for it — it fell through to consumeRegularEvent. But ConcordChannelListState
observes the addressable note at Address(13302, pubkey, ""), which never received
the event, so liveCommunities stayed empty.

Make ConcordCommunityListEvent a BaseReplaceableEvent (kind 13302 is a NIP-01
replaceable — fixed empty d-tag, address = (kind, pubkey, "")) and add the
consumeBaseReplaceable branch next to the kind-10009 list, so it lands in the
addressable cache exactly like its NIP-29 sibling. The plane-wrap path (1059 ->
concordSessions.ingest in DecryptAndIndexProcessor) was already correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 03:53:17 +00:00
Claude ec51d4020c feat(concord): stock-relay list import + single-screen community/channel browser
Two things that make the Armada-interop scenario actually usable:

- Bootstrap from the Concord stock relays: Account.importConcordCommunitiesFromStockRelays
  fetches this account's kind-13302 joined list from InviteRelayDictionary.STOCK (where
  the reference client publishes it — e.g. relay.ditto.pub — not the user's outbox) and
  folds the newest into LocalCache. Triggered on Concord-hub open (scoped so only Concord
  users reach those relays). Read-only/newest-wins, safe to repeat. This is why a community
  joined on Armada never showed up before: Amethyst only queried the user's own relays.

- The hub is now a single-screen browser: a community rail across the top plus an
  expandable accordion where each community reveals its #/lock/mic channels inline — browse
  community-first, then channels, without leaving the screen. Mounts the live plane
  subscription so channels fold in while browsing; tapping a channel opens its chat, the
  community avatar opens the full server view.

Still open for true two-client parity: verifying the 13302 encrypted-list JSON schema
against a real Armada-written event (field-name decode) and union-merging on write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 03:30:26 +00:00
Claude c4657d482e feat(concord): invite card, deep links, and Messages group-by-community
Completes the parity gaps with NIP-29 relay groups:

- Rich invite card (ConcordInviteCard): an invite link in note content now renders
  as a tappable card that peeks the kind-33301 bundle (Account.peekConcordInvite) to
  show the community name, instead of a bare link. Wired into RichTextViewer.
- Bare naddr (kind 33301) in ClickableRoute now shows an informative label rather
  than an empty addressable-note card (a naddr has no unlock token, so it can't be
  joined — only the full link can).
- External invite URLs open the app: AndroidManifest intent-filter for
  amethyst.social/invite/*, and MainActivity.uriToRoute maps the full URL (fragment
  included) to Route.ConcordInvite so the redeem flow keeps the token.
- Messages group-by-community view mode (ConcordViewMode INLINE/GROUPED), the
  analog of NIP-29's group-by-relay: GROUPED collapses each community's channels
  into one ConcordServerRoomNote row (rendered by ConcordServerRoomCompose, opens the
  channel list). Adds updateConcordViewMode + LocalPreferences persistence + feed
  invalidation + the ChatroomListKnownFeedFilter branch + a Messages-settings toggle.

Also resolves a silent merge artifact: main's markDmRoomAsRead(signedEvents.msg)
landed in the wraps-only broadcastPrivately overload (no signedEvents in scope);
moved it to the Result overload that carries .msg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 02:03:10 +00:00
Claude 8326f08f43 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-11 01:51:00 +00:00
Claude 73c6bd6180 feat(concord): members roster + edit-metadata screens, redesigned create form
Adds the two management screens the NIP-29 relay groups have but Concord lacked,
plus a visual overhaul of the create screen:

- Members roster (ConcordMembersScreen): owner + role-holders + banned, each with
  avatar/name and owner/admin/banned badge; overflow menu promotes/demotes (owner)
  and bans/unbans, gated on the viewer's authority. New AuthorityResolver.roleHolders()
  and bannedMembers() accessors expose the privileged roster (membership is otherwise
  key possession, so there is no silent-member list).
- Edit metadata (ConcordEditScreen): new write path — ConcordModeration.editMetadata
  (METADATA edition, entityId = communityId) + Account.editConcordMetadata; prefilled
  from the folded state, honored on fold only for MANAGE_METADATA holders / owner.
- Create screen redesign: shared ConcordMetadataFields with a circular icon hero
  (live preview from the URL) + section headers, mirroring the NIP-29 GroupImagePicker
  layout, replacing the bare stack of text fields.
- Routes ConcordMembers/ConcordEdit + nav registration + top-bar entry points on the
  channel-list screen (Members always, Edit for those who can manage metadata).
- Account.peekConcordInvite for the upcoming invite card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 01:50:49 +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 0fae8b4966 fix(concord): route taps on a Concord message to its channel
RouteMaker resolved Marmot and NIP-29 relay-group channel gatherers to their
chat screens but had no ConcordChannel branch, so tapping a Concord message
(from the Messages inbox row, a quoted note, go-to-conversation) fell through
to the generic thread view instead of opening the channel. Add the branch and
a routeFor(ConcordChannel) mirroring routeFor(RelayGroupChannel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 01:20:42 +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
Claude 9771d24cba fix(concord): fetch the kind-13302 community list at login
The account info/lists subscription loaded every other joined-list up front
(NIP-29 simple groups, relay lists, wallet, …) but not the Concord
joined-communities list, so a community joined on another client sharing the
same key (e.g. the Armada reference client) never surfaced: nothing REQ'd
kind-13302 for the account, leaving ConcordChannelListState empty until the
user created or redeemed an invite inside Amethyst.

Add ConcordCommunityListEvent.KIND to the up-front fetch, mirroring the NIP-29
SimpleGroupList rationale already there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 01:13:46 +00:00
Claude 7ebbd2e46e fix(concord): round the create-community FAB to match app convention
The hub's create FAB fell back to Material3's default rounded-square shape;
every other FAB in the app (RelayGroupChannelListScreen, MarmotGroupListScreen,
NewNoteButton, …) uses shape = CircleShape with a 24dp icon. Match it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 01:08:39 +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 1cb930f941 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-11 00:21:29 +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 b4fe6dffcd refactor(concord): move per-relay filter assembly to shared planner
The Android assembler was hand-rolling the plane-address -> per-relay
kind-1059 RelayBasedFilter collapse (with per-relay since). That logic is
platform-agnostic — RelayBasedFilter and SincePerRelayMap are both
commons/quartz types — so lift it to ConcordSubscriptionPlanner.relayBasedFilters
alongside the existing filtersByRelay one-shot variant. The assembler now only
does the account-dependent step (deriving channel planes from folded session
state) and delegates the collapse. Covered by a new planner unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 00:16:05 +00:00
Claude bafa0cca9a feat(concord): invite dialog shows a QR code + share sheet
The minted invite link now renders as a scannable QR (QrCodeDrawer) with a
system Share action (ACTION_SEND chooser) and a Copy fallback, instead of just
copying raw text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 23:55:20 +00:00
Claude 3f475fd5eb feat(concord): rich community/channel rows + inbox community icon
- ConcordHomeScreen: each community row now shows a circular avatar
  (RobohashFallbackAsyncImage, community icon or robohash fallback) and a
  pluralized channel count, re-read reactively on each Control-Plane fold.
- ConcordChannelListScreen: channel rows show a type icon (# public / lock
  private / mic voice) and an explicit empty state, styled like the relay-group
  channel list (thin dividers).
- Inbox row (ConcordRoomCompose): passes the folded community icon to ChannelName
  instead of null, so Messages shows the community avatar.
- ConcordChannel exposes communityIcon from the folded MetadataEntity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 23:53:49 +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
Claude f7ee2964db feat(concord): create screen uses the relay picker + icon field
Replaces the comma-separated relay text box with the app's RelayUrlEditField
(type a relay → NIP-11 name/icon autocomplete, tap to add), shown above a
removable list of chosen relays. Adds an optional community icon URL, plumbed
through createConcordCommunity → ConcordActions.createCommunity →
ConcordCommunityFactory into MetadataEntity.icon. Section header styling matches
RelayGroupMetadataScreen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 23:49:04 +00:00
Claude bfc77e80f6 feat(concord): rich message composer (@mention search, inline mentions, reply preview)
Replaces the bespoke plain-text composer with the app's standard chat input,
mirroring MarmotNewMessageViewModel: ConcordNewMessageViewModel drives
UserSuggestionState (type @ → avatar + name + NIP-05 dropdown, ranked by people
who've posted in the channel), and the field uses ThinPaddingTextField with
MentionPreservingInputTransformation + UrlUserTagOutputTransformation (npub
mentions render inline as colored @names), DisplayReplyingToNote for the reply
preview, and ThinSendButton. Send still routes through the Concord plane wrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 23:45:29 +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 b48cc2c117 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt
2026-07-10 22:53:52 +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 21c6e60e18 feat(concord): mobile "Make admin" / role toggle
Adds an owner-only role toggle to the Concord note quick-action menu, alongside
Ban:

- Account.concordAdminTarget gates the action to the owner (only rank 0 strictly
  outranks the position-1 Admin role, as the resolver requires), never the
  owner's own note or the owner as target, and reports whether the author is
  already an admin (via the new AuthorityResolver.rolesOf accessor).
- makeConcordAdmin mints a default Admin role (all management + moderation
  permissions, position 1) if the community doesn't have one yet, then grants it;
  removeConcordAdmin revokes via an empty grant.
- The menu item flips between "Make admin" and "Remove admin" and fires
  toggleConcordAdmin.

Extends ConcordModerationTest to cover rolesOf and revoke-via-empty-grant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 22:41:14 +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