Commit Graph
1100 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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
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 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
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 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 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 c29374f089 feat(relayauth): surface failed sends + per-relay forget/last-used
Consume the new give-up signal: RelayPublishFailureToastSubscription
(hosted in LoggedInPage) listens for onEventGaveUp and toasts "couldn't
deliver to <relay>", so a dropped send is visible instead of silent.

Rationale polish in the auth settings screen: each relay card now shows
"Last used N ago" and a Forget button that clears both the ALLOW/DENY
override and the accumulated rationale for that relay. The store gains
clearRationale + allLastUsed (default-implemented on the interface) and
records a last-used timestamp on each grant; clearDecision/clearRationale
now prune the shared url key only when a relay has neither an override
nor rationale left, so a partial clear never orphans the reverse-lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude 70b23b6999 feat(relayauth): remember and show why each relay was granted
Extend RelayAuthPermissionStore with grant rationale (purpose ->
counterparty pubkeys), default no-op so other implementers are
unaffected; DataStoreRelayAuthPermissionStore persists it per relay,
merging counterparties across grants. AuthCoordinator records the
rationale via ledger.recordGrant whenever it authenticates (auto-allow,
override, or approved prompt). The relay-auth settings screen adds a
"Why you're logged in to these relays" section: per relay, purpose-
grouped rows ("Send your private message to:", "Download posts from:")
with each counterparty's avatar + name. Unit-tested: grouping, cross-
grant merge, and that counterparty-less purposes aren't recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +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 1cffd8db18 fix: propagate relay acceptances into gift-wrap chains for chat relay icons
Chat rows render the inner rumor note of a NIP-17 message, but relay
attribution only landed there through narrow windows, so accepted relays
often never showed as icons:

- Duplicate deliveries were stranded on the wrap: a gift wrap re-delivered
  by a second relay hit the duplicate branch of consumeRegularEvent, which
  tagged the outer wrap note only and never re-processed the event. Extract
  the OK-path drilling (wrap -> seal -> rumor) into
  LocalCache.addRelayToNoteAndInners and call it from both the OK
  confirmation path (markAsSeen) and the duplicate EVENT path, replacing
  CacheClientConnector's private copy.

- Cross-thread visibility: Note.event, Note.relays, Note.flowSet and the
  innerEventId of GiftWrapEvent/SealedRumorEvent are written by decrypt/index
  coroutines and read lock-free on relay socket threads; a stale read parks
  an acceptance on the outer envelope permanently. Mark them @Volatile.

- Orphaned UI flows: RenderClosedRelayList/RenderAllRelayList and
  createMustShowExpandButtonFlows captured note.flow().relays.stateFlow once
  in remember/stateIn; MemoryTrimmingService.cleanObservers destroys the
  unobserved NoteFlowSet while the lifecycle is stopped, so resumed rows
  never saw another relay update. Wrap in a cold flow that re-resolves
  flow() on every collection start.

- Indexing latency: sent DMs waited for the ~1s newEventBundles batcher
  before the self-wrap was unwrapped and the message reached the chatroom,
  parking early OKs on the wrap. broadcastPrivately and
  sendNip04PrivateMessage now run the EventProcessor on the freshly
  consumed note immediately; the batched re-delivery is idempotent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
2026-07-10 22:29:41 +00:00
Claude 97fc801294 fix: NIP-13 compliance — refresh created_at at mining start, clean nonce tag
Findings from a full NIP-13 review:

- The spec recommends updating created_at while mining. Queue jobs now
  re-stamp the template to "now" when a worker picks them up (a post can
  wait behind other jobs, and a job restored after process death could be
  hours old); the restorer does the same. Scheduled posts are exempt —
  their future created_at is intentional. Anonymous posts re-stamp before
  mining against the throwaway key.
- PoWTag.assemble(nonce, null) serialized the literal string "null" as
  the third tag entry; a missing commitment now omits the entry entirely.
- New tests: PoWTagTest pins the NIP-13 example tag shape and the
  no-commitment round trip; a queue test asserts the created_at
  re-stamp at mining start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-10 22:22:04 +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
Claude 441413ea95 feat(concord): roles & moderation write path (CORD-04)
ConcordModeration builds the Control Plane editions for defining a role,
granting roles to a member, and banning/unbanning — each a kind-3308 edition,
plaintext-sealed and wrapped on the Control Plane. It chains the next version
onto the entity's current head (version+1, prevHash=head.hash) and unions the
banlist, taking the community's current editions as input.

Authority is enforced at fold time by the AuthorityResolver, not here: a round-
trip test confirms an owner can define an Admin role, grant it (member gains BAN),
ban/unban (version chaining heals), and that a grant forged by a non-outranking
troll is dropped by the fold.

- ConcordActions.controlEditions opens control wraps into editions
- ConcordCommunitySession exposes controlEditions()/controlPlaneKey() so edits
  chain onto live state
- Account.grantConcordRole / banConcordMember / unbanConcordMember publish the
  edition with an instant local echo (the session refolds it)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 21:56:06 +00: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
Claude 6ef1caef77 feat: polish the PoW surfaces — icon chip, animated banner, time estimate, note pill
- Composer chip: replaced the text button with a bolt icon carrying the
  difficulty as a small badge (primary-tinted when active, dimmed when
  off), matching the layered-icon language of the rest of the options
  row; the current choice is bolded in the override menu.
- Mining banner: pulsing bolt while the nonce search runs, per-job
  elapsed time driven by a 1 Hz clock ("Note • mining at 20 bits • 12s"),
  and proper close IconButtons instead of a text "×". PoWJobState now
  carries miningStartedAt for the elapsed display.
- Settings: a live "≈ 45s per post on this device" estimate under the
  difficulty picker, from a one-shot cached sha256 benchmark
  (PoWEstimator in commons) and the 2^d expected-attempts mean.
- Received notes: DisplayPoW's plain "PoW-24" text is now a compact
  secondaryContainer pill (bolt + bits) used by NoteCompose, thread view
  and chat messages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-10 21:22:27 +00:00
Claude e85de73e60 feat(concord): plane-wrapped reactions and replies (outbound)
Receiving reactions/replies already worked (they decrypt via channelRumors and
wire to their target Note by id). This adds the send side, which must NOT go
through the generic public/NIP-17 reaction path — a plaintext kind-7 would e-tag
the private rumor id onto public relays, and the NIP-17 path wraps to named
recipients, not the channel plane.

- ChannelChat.reaction/reply build kind-7/kind-9 rumors bound to channel+epoch
  (reaction e-tags the target; reply q-tags the parent)
- ConcordActions.buildChannelReaction/buildChannelReply wrap them on the plane
- Account.reactToConcordMessage + sendConcordChannelMessage(replyTo) publish the
  wrap with an instant local echo, factored through publishConcordWrap
- AccountViewModel.reactToOrDelete intercepts Concord notes (detected by the
  ConcordChannel gatherer) and routes to the plane-wrapped reaction
- ConcordChannelScreen wires onWantsToReply into the composer with a reply banner

Zaps already route correctly: a Concord message is an unsigned rumor, so the
existing isPrivateRumor() path forces a PRIVATE (NIP-57 encrypted) zap, same as
NIP-17 DMs. A fully on-plane nutzap (no public receipt) remains a future upgrade.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 21:20:07 +00:00
Claude 35647d0a5c feat: persist the PoW queue and shield it with a shortService foreground service
Backgrounding the app no longer pauses or loses mining:

- commons: PoWPublishQueue checkpoints template jobs through a
  PoWJobPersistence hook (saved on enqueue, removed on finish/cancel,
  deduped by id so restore is idempotent) and fires onQueueActive on
  every enqueue. PoWReplay flattens the live continuation into a
  PersistedPoWJob record (broadcast / to-relays / schedule) that can be
  replayed headlessly. Opaque enqueueWork jobs (reactions, reposts, gift
  wraps) and anonymous posts stay in-memory on purpose — a throwaway key
  and its content must never touch disk.
- amethyst: PowJobStore (atomic-rename JSON file, single-lane writer,
  3-day staleness purge) and PowJobRestorer, which re-enqueues an
  account's checkpointed jobs on login and finishes them via the replay
  descriptor. Composers now pass the matching PoWReplay for the public,
  group-thread and scheduled paths.
- PowMiningForegroundService: a shortService-type FGS started on every
  enqueue (~3 min guaranteed budget, no special permission) that keeps
  the process out of the cached-apps freezer while mining. Stops itself
  when the queue drains; on onTimeout it exits cleanly since the jobs
  are checkpointed. The notification is a live progress card
  (NotificationCompat.ProgressStyle): one track segment per post filling
  as jobs finish, indeterminate for a single post, per-kind text, a
  cancel-all action, and Live Updates rendering on Android 16+.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-10 21:08:51 +00:00
Claude 65502c7cda refactor(concord): land decrypted messages in LocalCache as real Notes
Follows the NIP-28/NIP-17 model instead of a private per-session flow. The
community session is now purely a decrypt-and-validate gate: each validated
inner rumor is handed to a sink (ConcordRumorSink) that the app wires to
LocalCache.consumeConcordRumor — landing it as a real Note keyed by rumor id
and attaching message-like kinds (9/1111) to the ConcordChannel.

Consequences: messages are first-class Notes, so reactions (kind 7), replies
(1111), deletes (5), OTS and zaps wire up automatically by id through the normal
consume path; the Messages inbox shows real last-message previews and updates
incrementally (filterRelevantConcordMessages, keyed on the ConcordChannel
gatherer) instead of feed() rebuilds; and the chat screen reuses the shared
ChannelFeedViewModel + RefreshingChatroomFeedView (full note UI) with only the
Concord-specific encrypted send path kept bespoke.

- ConcordActions.channelRumors returns validated bound rumors (all chat kinds)
- ConcordCommunitySession/SessionRegistry/SessionManager thread the sink; the
  messagesFlow/ConcordChatMessage projection is gone from the session
- LocalCache.consumeConcordRumor attaches gatherer before justConsume so the
  inbox incremental filter can route the row

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 20:56:32 +00:00
Claude 8988c98308 feat: add NIP-13 proof-of-work publishing with a fire-and-forget mining queue
Implements user-facing PoW publishing (#3317) on top of the existing
quartz miner:

- quartz: PoWMiner.run gains a cooperative isActive cancellation hook
  (checked every ~VALID_BYTES^2 hashes); PoWNostrSigner decorator mines
  kind-scoped templates pre-signature so it composes with every signer;
  GiftWrapEvent.create/NIP17Factory can mine the outer ephemeral-key
  wrap (never the seal/rumor); NostrSignerWithClientTag exposes
  prepareTags so mining runs over the final tag set.
- commons: PoWPublishQueue (FIFO, capped worker pool on
  Dispatchers.Default, per-job cancel, in-memory only — unmined posts
  are lost on process death, logged) and PoWPolicy (kind-group
  categories with a hardcoded NEVER list: auth, zap requests, NWC and
  bunker RPC, HTTP/Blossom auth, drafts, metadata and lists, OTS).
- amethyst: per-account synced settings (difficulty Off/16/20/24/28 or
  custom, per-category checklist) in Compose Settings; Post enqueues the
  template and returns immediately; reactions, reposts, reports, private
  notes, DMs and long-form route through the same shouldMine gate at
  their existing choke points; per-post PoW override chip in the
  composer options row; "Mining proof of work… (N in queue)" phase with
  per-job cancel in the broadcast banner.

Scheduled posts and anonymous posts mine against the correct key
(scheduled posts skip mining in v1); the client tag is applied to the
template before mining so signing never invalidates the nonce.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
2026-07-10 20:24:45 +00:00
Claude 8f0cbc77cf feat(concord): tappable Concord invite links in note content
RichTextParser now classifies `…/invite/<naddr>#<fragment>` URLs as a
ConcordInviteLinkSegment (cheap substring gate before the base64/bech32 parse),
and RichTextViewer renders them via ClickableConcordInviteLink — tap opens the
redeem flow, long-press copies the link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 20:00:32 +00:00
Claude 8f34f5f7d2 feat(concord): wire ConcordSessionManager into Account + giftwrap route
Adds ConcordSessionManager (commons) that keeps one folding session per
joined community in step with the concord list and turns folds into an
observable revision the app watches to re-derive subscription filters.

Account owns one per account; the giftwrap decrypt path (GiftWrapEventHandler)
routes recognized Concord plane wraps to it before the NIP-59 DM check, so
ephemeral-p Concord wraps fold instead of being dropped as undecryptable DMs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 19:38:10 +00:00
Claude faa9aa732e feat(concord): add ConcordSessionRegistry account-wide coordinator
Holds one live ConcordCommunitySession per joined community, fans inbound
kind-1059 wraps out to the owning session, and exposes the union of
control/channel plane addresses to subscribe. sync() reconciles sessions
against the joined list while preserving already-folded state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 19:30:55 +00:00
Claude 53c3a92288 feat(concord): add ConcordCommunitySession read-model
The live, stateful read-model of one joined community that a screen/ViewModel
binds to and a subscription feeds — bridging the pure logic to the UI:

- derives the Control Plane address up front (from the entry's secrets)
- ingest(wrap) routes by stream address: control wraps re-fold into a
  state StateFlow (metadata + channels + authority) and re-derive each channel's
  Chat Plane address; channel wraps re-project into per-channel message flows
- exposes controlPlaneAddress + channelAddresses() (what to subscribe to),
  state, membership(), and messagesFlow(channelId)
- thread-safe; unknown wraps (other communities) are ignored

Test: feed genesis control wraps -> "Nostrichs" + #general + OWNER membership +
the general plane becomes a known address; feed a channel message -> it lands in
#general's flow; a stray community's wrap is ignored. Green on :commons:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 19:23:43 +00:00
Claude 6178a4e3c6 feat(concord): add ConcordPlaneRegistry (route + decrypt inbound wraps)
The read-path counterpart to the subscription planner: maps derived plane
addresses (group_key.pk) to the keys that open them, so an inbound kind-1059 wrap
is recognized as Concord traffic and decrypted with the right per-plane key.

- registerControlPlanes(entries): control addresses, known from secrets alone
- registerChannels(entry, foldedState): channel addresses, known after the
  Control Plane folds
- route(wrap): if wrap.pubkey is a registered plane, open+verify and return the
  routed rumor with its plane kind/community/channel; else null

Because the wrap p-tag is ephemeral, address matching is the only route — a
non-member never registers the address, so never decrypts. Thread-safe.

Test routes a genesis control wrap to CONTROL and a channel message to CHANNEL
(right channel id + decrypted content), and rejects a wrap from another
community. Green on :commons:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 18:51:08 +00:00
Claude a1ae4220e9 feat(concord): add subscription planner (warm all joined planes)
The commons half of the "keep channels live" subscription, mirroring NIP-29's
RelayGroupMyJoinedGroupsFilterAssembler: turns the account's joined-communities
list into per-plane REQs by derived stream address (there is no #p=me for
Concord, so each plane is fetched by authors=[planePk]).

- ConcordSubscriptionPlanner.controlPlaneSubs: one Control Plane sub per joined
  community (known from the entry's secrets alone)
- channelPlaneSubs: one Chat Plane sub per channel once the Control Plane folds
- filtersByRelay: collapses subs to one kind-1059 author filter per relay
- ConcordActions.planeFilterFor(authors): multi-author plane filter

Test derives a community, asserts the control/channel sub addresses equal the
derived plane pks and that the per-relay filter carries both. Green on
:commons:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 18:48:12 +00:00
Claude e560df6edc feat(concord): add ConcordChannel model + LocalCache index
The channel model the shared chat UI renders, mirroring RelayGroupChannel:

- quartz ConcordChannelId(communityId, channelId): the stable channel address
  (analog of NIP-29 GroupId, but community-scoped rather than relay-pinned)
- commons ConcordChannel : Channel — name/voice/private from the folded Control
  Plane (no single relay-signed metadata event), relays() = the community relay
  set (a plane may be mirrored on several), membership from the authority
  resolver, canPost(), placeholderNote() for immediate Messages-list rows;
  updateFrom(state, relays, myPubKey) refreshes on each re-fold
- LocalCache: concordChannels LargeCache index + getOrCreate/getIfExists

Compiles across :quartz/:commons/:amethyst. Next: the decryption subscription
that folds inbound channel-plane wraps into these, then the Messages inbox hook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 18:25:52 +00:00
Claude 22e131e51c feat(concord): wire ConcordChannelListState into Account (kind-13302 home base)
The per-account home base for Concord Channels, mirroring RelayGroupListState:

- commons ConcordChannelListState observes the kind-13302 addressable note,
  exposes liveCommunities (joined secret-bearing entries) and liveServers
  (community ids), and follow/unfollow read-modify-write the self-encrypted list;
  a ConcordListRepository backs offline restore
- AccountSettings implements ConcordListRepository (backupConcordList +
  concordList/updateConcordListTo) and gains a concordViewMode setting
- Account instantiates concordChannelList and exposes joinConcordCommunity(entry)
  / leaveConcordCommunity(id), publishing the list via the account outbox

Concord's "in-between" shows here: same wiring as the NIP-29 list, but entries
carry the community secrets (decrypt yields root/salt/epoch/channel keys), so one
self-encrypted event both syncs membership and re-derives every plane. Compiles
across :commons and :amethyst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 18:11:17 +00:00
Claude 4672b60c9d feat(concord): add mobile foundation enums (membership + view mode)
First slice of the Android integration, mirroring NIP-29's commons model:

- ConcordMembership (OWNER/ADMIN/MEMBER/BANNED/NONE) derived from the folded
  owner-rooted AuthorityResolver + banlist — membership is key possession, roles
  layer moderation power, the banlist removes standing; with isMember/canModerate
- ConcordViewMode (INLINE/GROUPED) for how joined channels surface in Messages

Test classifies owner/admin/member/banned/stranger from a folded control plane.
Green on :commons:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 17:19:36 +00:00
Claude 51494d5330 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-10 17:05:56 +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 587387ba96 feat(concord): add commons ConcordActions (CLI-safe business layer)
Pure builders + relay-filter assembly + folding for Concord, usable from amy CLI
and the Android app (like DmActions, it never touches the network):

- plane key derivation (controlPlane/publicChannel)
- relay filters (planeFilter, bundleFilter, directInvitesFilter)
- createCommunity, foldCommunity (open control wraps -> editions -> live state)
- buildChannelMessage + channelMessages (open, bind-check, order oldest-first)
- invite helpers: inviteFor, mintInviteLink, parseInviteLink, openBundle
  (decrypt+validate), controlPlaneFor

Test covers the create -> fold -> send -> read round-trip and the mint -> parse
-> open -> read invite flow. Green on :commons:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-09 23:34:15 +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 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
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 4fca9ae25e feat: message-count + follows-in-group on discovery cards; split relay chip
Three group-discovery card changes:

- Reactive loaded-message count. The preview subscription streams the group's
  recent kind-9 chats into the channel note cache; the card now shows that count
  on the stats line ("12 members · 50+ messages"), updating live as messages
  arrive. Bumped the preview page 15 -> 50 so an active chat reads as "50+"
  (also a better warm-up); the display caps at DISCOVERY_MESSAGE_CAP.
- People-you-follow social proof. RelayGroupChannel.participatingFollows()
  intersects the relay-signed roster with the kind-3 follow set; the card shows
  an overlapping face pile + "%d people you follow" caption when non-empty.
- Split the relay chip's tap targets. Tapping the chip body now opens that
  relay's full group list (Route.RelayGroupServer); only the star toggles the
  relay favorite. Previously the whole chip favorited, which was easy to hit by
  accident.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 18:24:36 +00:00
Claude a4679da0a9 refactor: move GroupDiscoveryConstraint matcher to commons
The sealed GroupDiscoveryConstraint matcher (AllGroups/ByPeople/ByHashtags/
ByGeohashes/AnyOf) is pure over RelayGroupChannel + HexKey with no LocalCache,
eose-manager, or topNavFeeds dependency, so it belongs in :commons alongside
RelayGroupChannel where Desktop/CLI can reuse it. The amethyst dal keeps only
the platform-specific toGroupConstraints() mapping from the Android top-nav
filter set onto the shared matcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
2026-07-09 15:25:03 +00:00
Claude 69204f16cb fix: use Mutex instead of JVM-only synchronized in AuthApprovalPolicy
The synchronized intrinsic is only available on JVM/Android, breaking the
iOS (compileKotlinIosSimulatorArm64) build in commonMain. Replace the Any()
lock with a kotlinx.coroutines Mutex + withLock, which is KMP-common and
safe here since all three store methods are already suspend functions.

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