Commit Graph
17609 Commits
Author SHA1 Message Date
Claude 9075fb39b8 feat(quartz): add BOLT12 offer list (NIP-2421 kind 10058)
The NIP was updated to publish a recipient's BOLT12 offer(s) in a dedicated
replaceable event (kind 10058, `bolt12_offer`) instead of a kind:0 field — this
is what ties an offer to a Nostr identity (the author's signature) and gives
BOLT12 zaps the send-side addressability that lightning zaps get from lud16.

- Bolt12OfferListEvent (kind 10058): a BaseReplaceableEvent holding one or more
  `["offer","lno1..."]` tags (reusing OfferTag), with offers()/firstOffer()
  accessors and create/updateOffers factories (mirrors ChatMessageRelayListEvent).
- Registered in EventFactory + a KindNames display entry.
- Tests: offers round-trip + factory typing, malformed-offer-tag filtering, and
  updateOffers replacing the offer set while keeping other tags.

Discovery: a payer fetches the recipient's latest 10058 and picks an offer. The
app-side caching, subscription, editor, and payment intent follow in later commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 19:18:27 +00:00
Claude 362844ee73 Merge remote-tracking branch 'origin/main' into claude/nip-2421-pr-review-6znvdd
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt
2026-07-23 21:37:47 +00:00
Claude 0b6a70ad26 fix(bolt12): audit fixes — offer binding, verified-only counting, lower-amount dedup, codec hardening
From an adversarial audit of the BOLT12-zap feature.

Security / correctness:
- Offer↔invoice binding: `cryptoVerified=true` was asserted even when the offer had
  no `offer_issuer_id` or used blinded paths — cases where the invoice's node key is
  payer-chosen and can't be tied to the offer. An attacker could self-sign a "verified"
  proof having paid nothing. Now cryptoVerified requires the invoice to be provably the
  offer's (issuer_id present, no paths, invoice_node_id == issuer); unbindable proofs are
  accepted but flagged unverified, not verified. Definite contradictions still hard-reject.
- Counting: `updateZapTotal` now counts ONLY crypto-verified BOLT12 zaps. An unverified
  (compressed / unbindable) proof carries a self-chosen preimage+amount with no settled-
  payment guarantee, so counting it let anyone inflate a note's total for free. Unverified
  entries stay stored + shown (dimmed), never summed.
- Dedup: `innerAddBolt12Zap` now honors the NIP's "count the LOWER amount for the same
  payment hash" rule (was order-dependent last-writer-wins, inflatable by re-publishing a
  bigger amount tag). Keeps the stronger verification flag.
- Precision: divide millisats in BigDecimal, so fractional sats survive and match the
  millisat-native lightning column (was integer `/1000`, flooring sub-sat zaps to 0).

Codec hardening (quartz):
- TLV length now range-checked (was a signed compare that let a high-bit BigSize length
  slip through and get truncated by toInt()).
- BigSize enforces minimal encoding (also rejects >=2^63 values that read back negative).
- bech32 alphabet membership is O(1) via a lookup table (was O(32n) indexOf per char).

UI:
- ReusableZapButton's "you zapped" gate now includes bolt12Zaps (and nutzaps/onchain),
  so a BOLT12-only zap correctly shows the zapped state.
- The reactions gallery renders the blank/unknown author for anonymous zaps, matching the
  standalone card (was showing the throwaway ephemeral key's avatar).

Tests: validator issuer-less-offer downgrade; TLV non-minimal-BigSize + oversized-length
rejection; model lower-amount dedup (both orderings), verified-only counting, and
fractional-sat survival. NoteBolt12ZapTest 6→8, Bolt12ZapValidatorTest 11→12, TlvTest 6→8.

The audit also surfaced a NIP-level gap that is NOT fixable in code and is captured in the
plan doc: there is no offer↔recipient-identity binding, so even a crypto-verified proof
only proves payment to the *embedded* offer, not to the p-tagged recipient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-23 21:17:01 +00:00
Vitor PamplonaandGitHub acebf96eb9 Merge pull request #3684 from vitorpamplona/claude/pow-miner-cancel-behavior-18ckmk
Add "send without PoW" option for template posts during mining
2026-07-23 16:46:45 -04:00
Claude 3e19b76343 perf(bolt12): fail-fast validation, skip redundant verify, precompute merkle tags
Speed:
- Bolt12ZapValidator reorders checks cheap-to-expensive: all structural, cross-event,
  and payer-proof binding checks run first; the schnorr signature + proof crypto
  verifications run only once an event has passed them. A malformed or mismatched
  event now rejects with zero schnorr ops. Cannot change accept/reject, only which
  reason a doubly-invalid event reports.
- validate() gains verifyEventSignature (default true); LocalCache.consume passes
  false since the relay pipeline already verified the outer event — removing a
  redundant schnorr on every ingested zap (3 verifies instead of 4).
- Bolt12Merkle precomputes SHA256("LnLeaf")/SHA256("LnBranch") once and hashes the
  per-call "LnNonce"||first-tlv tag once per rootHash instead of once per record.

Tests:
- New validator rejections: preimage-mismatch, invalid-invoice-signature,
  payer-tag-mismatch, proof-does-not-match-offer, plus the verifyEventSignature
  skip-flag both ways (fixture gains corruptPaymentHash / breakInvoiceSignature).
- New commons NoteBolt12ZapTest: millisat→sat total, dedup by payment_hash,
  verified-not-downgraded-by-unverified, remove-by-source, clearChildLinks, and
  combined totals.

Docs: quartz/plans/2026-07-23-bolt12-zap-interop-vectors.md captures the two
upstream-gated follow-ups (vector-driven interop test + compressed-proof merkle
reconstruction) for when lightning/bolts#1346 merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-23 20:43:02 +00:00
Claude 33fb3a54b2 feat: account for and display BOLT12 zaps everywhere lightning zaps are
Wires the receiving side of NIP-XX BOLT12 zaps (kind 9736) into every place a
NIP-57 lightning zap is counted or shown. Sending is intentionally left for
later. Modeled on the lightning-zap scheme (synchronous, the proof carries the
amount, counted the moment it validates) rather than the onchain scheme (async
chain backend, PENDING/CONFIRMED, CONFIRMED-only) — BOLT12 proof verification is
a self-contained synchronous check, so no resolver/backend is needed.

Model (commons):
- Bolt12ZapEntry + Note.bolt12Zaps map keyed by the proof's invoice_payment_hash
  (the spec dedup key); addBolt12Zap/removeBolt12ZapBySource; folded into
  updateZapTotal (millisats → sats) alongside lightning/onchain/nutzap amounts;
  wired into clearChildLinks, moveAllReferencesTo, removeNote,
  hasZapsBoostsOrReactions, hasZapped, and the isZappedBy family.

Ingestion (LocalCache):
- consume(Bolt12ZapEvent): validate synchronously via Bolt12ZapValidator, then
  addBolt12Zap on the resolved targets (e / a / profile); computeReplyTo and
  live-activity channel routing branches; dispatch case.

Subscriptions: added kind 9736 to every filter carrying LnZapEvent.KIND
(notifications, replies/reactions to notes & addresses, profile received-zaps,
live-activity goal + messages, nest room + collectors, notification dispatcher,
shared NotificationKinds, app-functions).

Aggregation / notifications: UserProfileZapsViewModel (mapper), NotificationSummaryState
(both passes), NotificationFeedFilter (kinds, zap-receipt detection, payer author
resolution, muted-thread + own-event gates), NotificationKinds own-event exception,
ThreadAssembler.anchorsItsOwnThread, and the commons live-activity aggregators
(RoomZapsState, LiveStreamTopZappers, NestViewModel).

UI: RenderBolt12Zap standalone card (styled like the lightning card, labeled
BOLT12) wired into NoteCompose + ThreadFeedView; Bolt12ZapGallery in the
reactions row (payer avatars + amounts, unverified/compressed proofs dimmed);
reaction-row counter gate; KindNames / KindDisplayName entries.

Note: validated BOLT12 zaps are counted immediately; a compressed proof whose
signatures aren't yet verifiable (pending lightning/bolts#1346 merkle
reconstruction) is stored with cryptoVerified=false and dimmed in the gallery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-23 20:08:46 +00:00
Claude 7a3fc48f07 feat: offer "send without PoW" when abandoning a mining job
Cancelling a proof-of-work mining job used to silently discard the post
(the sign+broadcast continuation only ran on a successfully mined
template). Users had no way to publish the note un-mined once they'd
started waiting.

Now abandoning a template post asks what to do instead of discarding:

- The × on the mining banner opens a dialog with "Send without PoW",
  "Discard post", or tap-away to keep mining. Only shown for jobs that
  carry a plain un-mined fallback (template posts); opaque work jobs
  (reactions, reposts, anonymous posts, gift wraps) keep the direct
  cancel since they have no template to fall back to.
- The mining foreground-service notification gains a "Send now" action
  that publishes every eligible queued post without proof of work.

Implementation: the queue keeps the un-mined publish continuation
alongside the miner. sendWithoutPow() sets a flag the worker picks up on
its next isActive poll; the miner aborts and the plain template is
published through the same sign+broadcast path the mined template would
have used, off the worker pool. PoWJobState exposes canSendWithoutPow so
the UI knows which jobs support it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012kLVFV7ps4HfXDDJPNqi82
2026-07-23 20:00:16 +00:00
Vitor PamplonaandGitHub a6e6903f6d Merge pull request #3682 from vitorpamplona/claude/buzz-repo-analysis-7k54ga
Add Buzz protocol support with workspace, DM, and agent features
2026-07-23 15:45:09 -04:00
Claude a657a9698d fix(buzz): use assertTrue instead of assert in ForumCommentEventTest
Kotlin/Native marks the stdlib `assert()` with `@ExperimentalNativeApi`, so
the iOS test target (`compileTestKotlinIosSimulatorArm64`) failed to compile
without an opt-in — while JVM/Android were fine. Swap it for `assertTrue`
from `kotlin.test`, which needs no opt-in on any target and matches the
assertions already used in this file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 19:19:27 +00:00
Claude 2ec1744c92 feat(quartz): add BOLT12 zaps (NIP-2421) protocol layer
Implements the quartz-side of the proposed "BOLT12 Zaps" NIP
(nostr-protocol/nips#2421): public, self-verifying zap events that prove a
BOLT12 payment without an LNURL server or recipient-operated receipt publisher.

Events (nip-88 style templates/tags/builders):
- Bolt12ZapEvent (kind 9736) and Bolt12ZapIntentEvent (kind 9737)
- shared tags: amount (msats), offer, proof, P (payer), zap_id, description
- registered both kinds in EventFactory

BOLT12 decoding (new, no existing KMP library):
- Bolt12Bech32: canonicalization (+ continuation / whitespace) + no-checksum,
  no-length-limit bech32 for lno1 offers and lnp1 payer proofs
- Tlv: BigSize codec, TLV stream reader/writer, tu64 helpers
- Bolt12Offer / Bolt12PayerProof parsers (proof TLV types per lightning/bolts#1346)
- Bolt12Merkle: BOLT12 tagged-hash + signature merkle root + signature digest

Validation:
- Bolt12ZapValidator runs the NIP's steps (structure, embedded-intent match,
  payer-proof binding: invreq_payer_note == nostr:nipXX:<intent-id>,
  invoice_amount == amount) and returns a typed result with the payment-hash
  dedup key
- Bolt12ProofVerifier checks preimage->payment_hash and the invoice/proof
  BIP-340 signatures for fully-disclosed proofs; compressed proofs are reported
  as unverified pending the (still-draft) lightning/bolts#1346 test vectors
- Bolt12ZapBuilder assembles+signs the intent and the final zap

Tests: 24 commonTest cases covering bech32/TLV/merkle round-trips, event tag
structure + factory typing, and validator accept/reject paths (self-signed
BOLT12 fixtures exercise the full merkle + schnorr path).

Note: this is the receive/verify + assembly layer only. Origination is blocked
on the upstream BOLT12 payer-proof spec merging and a wallet/NWC rail exposing
lnp proofs; no Amethyst payment rail returns one today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-23 19:15:30 +00:00
Claude 210aa86c2f Merge remote-tracking branch 'origin/claude/buzz-repo-analysis-7k54ga' into claude/buzz-repo-analysis-7k54ga 2026-07-23 18:43:47 +00:00
Claude 9ab1c650a9 feat(buzz): move workspace add-people & invite-link into top-bar overflow menu
The two workspace-owner actions (Add people to this workspace, Create invite
link) sat as an inline button row above the channel list. Move them into a
3-dot overflow (MoreVert) menu in the top-right of the workspace top bar,
matching the Concord community pattern. Fold the invite-mint flow (spinner,
result Copy/Share dialog, error dialog) into the new BuzzWorkspaceOverflowMenu
and drop the now-obsolete BuzzInviteMintButton.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 18:43:24 +00:00
Vitor PamplonaandGitHub 66108d8aa1 Merge pull request #3680 from davotoula/feat/644-warn-dms-from-reported-senders
Warn users when a DM comes from an account that people they follow have reported
2026-07-23 14:41:30 -04:00
Vitor PamplonaandClaude Opus 4.8 3a9d9a7b5d fix(buzz): show bare Buzz reactions (e-tag only) in Notifications
A Buzz like is a bare kind-7 `["e", <msg-id>]` — no `p` tag (unlike NIP-25,
which p-tags the reacted author) and no `h` tag. The notifications filter's
p-tag gate and follow filter therefore both dropped it, so a like on my Buzz
message never surfaced even in Global mode, despite the reaction being loaded
(it rendered as a chip in the open conversation).

Recognize a reaction/repost that carries NO `p` tag whose reacted target — the
last `e` tag, already loaded — is my own note, and let it bypass the follow
filter and satisfy the p-tag gate (like a Concord reaction). Scoped to the
no-`p`-tag case so well-formed NIP-25 reactions keep their normal routing, and
to an already-loaded target so it can't accept blindly. The per-kind gate
already resolves the same target author, so it needs no change.

Verified on emulator: a 👍 + ❤️ on my Buzz DM message now appears on the
Curated notifications tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:35:41 -04:00
Vitor PamplonaandClaude Opus 4.8 8982af5752 feat(buzz): notify on reactions/zaps to my Buzz DM messages
The group-notification query (reactions/zaps/reposts on my messages inside a
NIP-29 group, kind-7 etc. scoped #p=me + #h on the host relay) sourced its #h
set from the published kind-10009 group list — which deliberately excludes Buzz
DM channels (membership is server-side, tracked in BuzzDmChannels). So a
reaction on my DM message was only ever loaded as a chip inside the open
conversation, never surfacing on the Notifications tab.

Include DM channels (from BuzzDmChannels, minus hidden ones) alongside the
joined groups in both the live-tail (AccountNotificationsEoseFromInboxRelays-
Manager) and backward-history (AccountNotificationsHistoryEoseManager) queries,
reusing the same filterGroupNotificationsToPubkey builder, and re-subscribe when
a DM is discovered/hidden. Regular-channel reactions already worked via the
joined-group path; this closes the DM gap the same way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:35:41 -04:00
Claude 8600484d0b Merge remote-tracking branch 'origin/main' into claude/buzz-repo-analysis-7k54ga 2026-07-23 18:31:44 +00:00
davotoula 8b7dac5c0b Styling updates 2026-07-23 19:24:43 +02:00
davotoula 4405e20eb7 Code review:
- docs(dm): note the profile Reports tab also reads reportsNamingUser
- refactor(dm): push report-tag typing to quartz and simplify the warning stack
- fix(dm): narrow report indexing and address final review findings
- perf(dm): resolve a 1:1 chat row's counterpart once per row
2026-07-23 19:23:43 +02:00
davotoula 3e1d537eac feat(dm): flag reported counterparts on the chat list row
fix(dm): dedupe reporter avatars and align warning card styling
feat(dm): warn in the room when the counterpart is reported by a follow
feat(dm): expose a report-warning flow per user
refactor(reports): extract reusable reportTypeLabel composable
feat(reports): index author-named reports even when they target an event
feat(reports): add pure DM report-warning classifier
feat(reports): add additive reportsNamingUser index to UserReportCache
2026-07-23 19:23:43 +02:00
davotoula b2fa1d3b92 upgrade agp 2026-07-23 18:53:54 +02:00
David KasparandGitHub 5c4a7e8620 Merge pull request #3679 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-23 17:35:07 +01:00
Claude 647997f895 refactor(buzz): audit pass — fix draft-save invite bug, races, GC churn
Batch of correctness, performance, and code-quality fixes across the Buzz
workspace feature surfaced by a full audit of the branch's modified files:

- ChannelNewMessageViewModel: agent auto-invite ran inside createTemplate(),
  which sendDraftSync() calls per keystroke — so @mentioning a member
  published real kind-9000 invites while still typing. Move the invite to the
  send path (sendPostSync) and stage the pending mentions instead.
- AgentConsoleViewModel: fix a decryptCache read/reload race by guarding
  reloadFromCache (not refresh) under the mutex; bound observerSeen growth.
- BuzzNewDmViewModel: broaden start()'s catch so signer/IO/timeout failures
  surface as an error instead of leaving Start stuck on "Sending"; rethrow
  CancellationException.
- BuzzPresenceState / BuzzAgentActivityState: replace per-record full-map
  copies with persistent maps (structural sharing) to cut GC churn; the
  StateFlow now holds the map directly.
- RelayGroupChannelListScreen / RelayGroupMembersScreen / RelayGroupTopBar /
  RenderBuzzNotes: correct remember/LaunchedEffect keys so state rebinds when
  the relay or channel changes.
- RelayGroupDiscoveryFeedFilter: hoist joinedGroupIds() out of the per-item
  matches() loop.
- RelayGroupChannel / BuzzWorkspaceStates: @Volatile the fields read off relay
  dispatcher threads.
- BuzzInviteMinter: build the request URL through HttpUrl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 16:32:26 +00:00
Vitor PamplonaandClaude Opus 4.8 995d0e8489 feat(buzz): render Buzz DM notifications as messages, not posts
Follow-up to the Buzz-DMs-in-Notifications work: a Buzz DM on the Notifications
tab rendered like a public post and its actions didn't fit a DM.

- Render as a conversation: new RenderRelayGroupMessage draws a
  RelayGroupChannelHeader (titled by the other participant for a DM) above the
  message body — the group/DM analog of RenderChatMessage/RenderChannelMessage.
  NoteCompose routes a group-scoped kind-9 (ChatEvent with a RelayGroupChannel
  gatherer) and kind-40002 (StreamMessageV2Event) to it. The in-conversation
  chat screen renders via RefreshingChatroomFeedView, not NoteCompose, so its
  bubbles are unaffected.

- Reply goes into the conversation, not a public kind:1111: routeReplyTo now
  detects the gathering RelayGroupChannel (mirroring the Marmot/Concord cases)
  and opens the group/DM instead of falling through to Route.GenericCommentPost.

- Action row matches the card: a relay-group message (incl. a Buzz DM) hides
  Repost and Share — both would reference a membership-gated group event that
  non-members can't fetch, and a DM shouldn't be rebroadcast. Reply/like/zap stay.

Verified on emulator: the DM card shows the participant header + body + a
reply/like/zap-only row, and tapping reply opens the DM conversation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:03:56 -04:00
Vitor PamplonaandClaude Opus 4.8 eeeaab3c43 feat(buzz): surface Buzz DMs in the Notification feed
A Buzz DM is a relay-authoritative NIP-29 group whose messages carry no `p`
tag, so nothing made them eligible for the Notifications tab and nothing
fetched them app-wide (discovery was scoped to the open DM inbox, which only
pulls 44100 + 39000 — never the message bodies). Two halves fix that:

- NotificationFeedFilter now early-accepts a group chat message (kind-9 or
  kind-40002 — the deployed relay uses both) when it resolves to a `t=dm`
  channel whose 39000 participants include me, honoring the same "Messages in
  notifications" toggle and never notifying for my own message. LocalCache
  gains `getRelayGroupChannelForContent`, the read-only reverse-lookup this
  needs (same serving-relay-then-single-channel keying as the consume path).

- An always-on discovery (BuzzDmDiscoveryPreload) subscribes 44100 #p=me across
  joined workspaces into the new BuzzDmChannels registry and fetches each DM's
  39000 directory; BuzzDmJoinedChatTailFilterAssembler then keeps those
  channels' recent messages warm app-wide (reusing the joined-group #h tail),
  excluding hidden DMs. Both mount in LoggedInPage. This is what makes a Buzz
  DM show on Notifications / in push without opening the conversation.

Tests: BuzzDmChannels registry; and a LocalCache resolution test proving a
40002 and a kind-9 message both resolve back to their DM channel (and a
non-dm channel does not).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:03:56 -04:00
Claude 20384f5481 feat(buzz): add people to a channel/community + mint invite links
Three owner/admin add-people paths (the relay enforces the role gate):

- Channel add-member: an "Add member" FAB on the channel members screen
  opens a user-search dialog that publishes a NIP-29 kind-9000 put-user for
  the picked pubkey (plain member).
- Community add-member: an "Add people to this workspace" action in the Buzz
  community view publishes the Buzz relay-admin add-member command (kind
  9030) so the relay updates its NIP-43 membership list (13534). Wires the
  existing quartz RelayAdminAddMember/RemoveMember events into Account +
  AccountViewModel (addCommunityMember / removeCommunityMember).
- Invite-link mint: BuzzInviteMinter does the Buzz-specific, NIP-98-signed
  POST /api/invites on the relay host (via HTTPAuthorizationEvent + the
  trusted-relay OkHttp client) and returns {code, url, expires_at}; a
  "Create invite link" button surfaces the link with Copy / Share.

The user search is extracted into a reusable BuzzAddPeopleDialog shared by
the channel and community add flows. /api/invites is Buzz-proprietary (only
NIP-98 is standard); community add uses Buzz's 9030 admin command, since
NIP-43 itself has no owner-initiated add.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 15:48:50 +00:00
vitorpamplonaandgithub-actions[bot] 8c0bee526a chore: sync Crowdin translations and seed translator npub placeholders 2026-07-23 15:48:02 +00:00
Vitor PamplonaandGitHub 6a38d1330f Merge pull request #3678 from vitorpamplona/claude/home-screen-tab-row-padding-3yqyvq
Fix excessive vertical spacing in home feed when no live bubbles
2026-07-23 11:45:34 -04:00
Vitor PamplonaandGitHub ebe5b572a6 Merge pull request #3677 from vitorpamplona/claude/zap-devs-card-modernize-1dib1z
Redesign ZapTheDevsCard with gradient hero header and improved UI
2026-07-23 11:41:14 -04:00
Claude 9b9a3f12a9 fix(home): drop dead top padding when there are no live bubbles
The home feed always emitted a live-bubbles item followed by a 5dp
Spacer, even when the live section (live streams, geohash/ephemeral
chats) was empty — which is the common case. With no bubbles to draw,
that Spacer reserved dead vertical space at the very top of the list,
stacking on top of the LazyColumn's FeedPadding and the first note's own
top padding. The result was an oversized gap between the top-bar divider
and the first author's picture, most noticeable when the New Threads /
Replies tab row is hidden (single tab) so the divider sits right under
the top bar.

Move the row and its trailing Spacer inside a non-empty check so they
only render when there are actual bubbles. When the live section is
empty the item now collapses to zero height, bringing the home feed's
top gap in line with every other feed screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTFiQykXfCShHUCA3yM2pN
2026-07-23 15:28:44 +00:00
Claude 196aeaf974 feat: modernize the Zap the Devs donation card
Give the donation card a bolder, more exciting look:
- Add a gradient hero header (bitcoin orange → amethyst purple) with a
  glowing circular bolt badge and a larger, white title
- Round the card corners (16dp) and add elevation for a modern, floating feel
- Overlay the close button on the hero and keep the description, version
  credit, zap splits and donate button in a clean body section

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bM2CKE5Mnao4FG85HPuAV
2026-07-23 15:16:45 +00:00
Vitor PamplonaandClaude Opus 4.8 a9fcd1c19c feat(buzz): Concord-style threading for Buzz chat replies
Concord threads chat messages with kind-1111 minichat comments, but Buzz relays
reject kind-1111 — so Buzz replies fell back to inline 40002 and rendered as flat
quotes, never opening a thread.

Buzz already threads over 40002 NIP-10 markers (matching block/buzz): a reply-marked,
non-`broadcast` 40002 is a thread reply; a `broadcast=1` one is an inline timeline
sibling. Teach Amethyst's minichat to speak that dialect:

- Add `isMinichatReply(event)`: kind-1111 CommentEvent, OR a Buzz 40002 thread reply
  (reply-marked, non-broadcast). Used by all three read sites so they agree.
- `LocalCache.computeReplyTo`: link a 40002 thread reply into its parent's replies.
- Timeline filter, minichat reply-count, and minichat feed now key on that predicate,
  so 40002 thread replies leave the timeline, get counted on the "N replies" chip, and
  render inside the minichat.
- Send: an INLINE reply on Buzz sets `broadcast=1` (stays inline); MINICHAT omits it
  (threads). `sendMinichatReply` posts a 40002 thread reply on Buzz instead of kind-1111.

Verified on device: a message shows an "N replies" chip, opens the Thread view with the
root + replies + composer, replies post and increment the count, and thread replies no
longer appear inline in the main chat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 09:37:41 -04:00
Vitor PamplonaandClaude Opus 4.8 f2dd8cd0f5 fix(buzz): keep DMs out of the Relay Groups discovery list
The Relay Groups discovery feed listed every kind-39000, including Buzz DM channels
— private 1:1 conversations that belong in the DM section, not a browsable group
list. Exclude channels whose 39000 carries the NIP-29 `hidden` tag (which Buzz sets
on t=dm channels) from both the constraint match and the "My Groups" path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:48:14 -04:00
Vitor PamplonaandClaude Opus 4.8 b713f6061a feat(buzz): title a DM by its participant, hide forum/share for DMs
A Buzz DM channel showed the generic "DM" metadata name and offered Forum-threads
+ Share actions that don't fit a private 1:1 conversation.

- Title the DM by the OTHER participant's display name (from the 39000 inlined `p`
  tags), reactively resolved, falling back to the channel name until it loads.
- Hide the Forum/Threads button and the Share button when the channel is a DM
  (isBuzzDm): a DM has no forum, and its private, membership-gated naddr is
  meaningless to share.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:13:29 -04:00
Vitor PamplonaandClaude Opus 4.8 818f5c0bc7 feat(buzz): forum thread replies header, empty state, and inset-correct scaffold
Match Buzz's reference forum thread and fix insets:
- Add a "{N} replies" count header and a "No replies yet" empty state (flat root +
  replies, mirroring block/buzz's ForumThreadPanel — replies are NOT hierarchical).
- Use DisappearingScaffold instead of a plain Scaffold so the composer + list get the
  app's shared imePadding + navigation-bar insets (same as RelayGroupChatScreen); the
  composer is content at the bottom of the Column, not a Scaffold bottomBar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:01:38 -04:00
Claude 4421df9695 fix(buzz): keep Agent Console Costs + Personas live so they load after auth
The console did a single fetch on bind and read the cache back immediately,
which races the NIP-42 auth handshake: the warm-auth fetch can return before
the authenticated read actually delivers, so personas (30175) and turn
metrics (44200) landed in LocalCache a moment after the one-and-only
reloadFromCache — leaving both tabs empty with no way to refresh (the VM's
bind guard blocks a re-fetch on re-entry).

Adds a live subscription (startWatching/stopWatching, mounted for the
console's whole lifetime) for 44200 #p=me + 30175 authors=me on the
community relay; each arriving batch re-derives both tabs from LocalCache.
The relay re-serves the subscription once the connection authenticates — the
same path that unlocks the channel roster — so the console now populates on
its own instead of showing nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 03:35:39 +00:00
Claude bfc6862be4 feat(buzz): auto-invite mentioned non-members into a workspace channel
Mentioning someone who isn't yet a member of a Buzz workspace channel now
adds them (kind-9000 put-user) before the message is published, so the
`@`-mention resolves to a real member — mirroring Buzz's own composer, which
is how you pull a bot into a channel by naming it.

Gated on the sender being able to moderate (only a moderator can issue
kind-9000; it's a silent no-op otherwise), skips self and already-present
members, and is best-effort so a failed add never blocks the message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 03:29:27 +00:00
Claude 7ae96ae3ce feat(buzz): presence dots, collapsible sections + unread + stars, canvas editing, bot "Working…" indicator
Brings the Buzz community + member screens closer to Buzz's own clients:

- Presence dots (BuzzPresenceState → online green / away amber) overlaid
  on DM-row and member-list avatars; offline/unknown shows nothing rather
  than implying we track a peer we don't.
- Community view sections (Channels / Forums) are now collapsible, each
  channel row carries an unread dot (relayGroupChannelHasUnreadFlow) and a
  star toggle; starred channels float to the top. Stars persist
  device-globally (BuzzChannelStars + BuzzChannelStarPreferences), mirroring
  the joined-workspaces store.
- Canvas editing: the canvas screen gains an edit mode that publishes a
  fresh kind-40100 CanvasEvent to the channel's host relay (last-write-wins);
  the top-bar canvas button now shows on any Buzz channel so an empty one can
  be created.
- Bot "Working…" indicator: a process-wide BuzzAgentActivityState, fed by a
  members-screen observer (24200) subscription, lights a live "Working…" line
  next to an agent currently emitting frames; tapping opens the community's
  Agent Console. Owner-scoped by nature (frames are #p=owner).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 03:28:48 +00:00
Vitor PamplonaandClaude Opus 4.8 bb88a18474 feat(buzz): use the full chat composer for forum replies
The forum thread's reply field was a bare OutlinedTextField. Replace it with the
same rich composer chats use (EditFieldRow): emoji + @mention suggestions, media
attach, drafts, and the typing indicator, with the standard send button styling.

Reuse works by making ChannelNewMessageViewModel.createTemplate() protected/open
and adding ForumReplyNewMessageViewModel, which overrides only the built event so
a send publishes a kind-45003 ForumCommentEvent scoped to the open thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:21:42 -04:00
Vitor PamplonaandClaude Opus 4.8 dbf9b29e25 feat(buzz): render forum posts as threads, not chat bubbles
Buzz forum roots (kind 45001) and comments (45003) were consumed onto the chat
timeline (consumeBuzzTimelineEvent → addNote), so a forum post showed up as a
kind-9 chat bubble and replies leaked into the chat feed.

Route them like the content they are:
- 45001 → the group's Threads collection (addThread) via a new consumeBuzzForumPost,
  so it surfaces in the forum/Threads view; 45003/45002 are now store-only.
- ThreadRow renders a ForumPostEvent (body-as-heading, no title) alongside kind-11.
- The Threads REQ (RELAY_GROUP_THREAD_KINDS) now also fetches 45001/45003.
- A forum-type channel (channel_type=forum) opens the Threads view directly from
  the community Forums section instead of the chat view.
- New BuzzForumThreadScreen: the root post + its 45003 replies + a reply composer
  (ForumCommentEvent.build). Own replies aren't re-delivered on the open sub, so a
  send restarts the REQ to re-fetch the thread.

Verified on device: forum posts list as threads, open to root+replies, replies post
and appear; the forum channel's chat no longer shows the posts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:10:57 -04:00
Vitor PamplonaandClaude Opus 4.8 681bd1b389 fix(buzz): recognize NIP-43 community membership in the channel gate
A Buzz community member (kind 13534 roster / 8000-8001 deltas) can read and post
across every channel on the relay, but `RelayGroupChannel.membershipOf()` read
only the per-channel NIP-29 roster (39001/39002), so a community member/admin who
wasn't explicitly added to a private channel resolved as NONE and was wrongly
gated (Join lock, hidden composer).

Add `BuzzCommunityMembership`, a per-relay registry fed by the relay-signed NIP-43
events, and consult it as a Buzz-only fallback in `membershipOf` (mapped to MEMBER
only, never channel ADMIN, to avoid over-granting per-channel moderation). Wire
`LocalCache.consume` for kinds 13534/8000/8001 to update the registry (LWW on
created_at) and poke the relay's channels so the gate re-renders live.

This complements the open-channel fix: open channels no longer require membership
at all, and members of private channels are now recognized community-wide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:59:47 -04:00
Vitor PamplonaandClaude Opus 4.8 709ab6565b fix(buzz): don't gate posting/Join on the roster for open channels
A Buzz workspace channel you can participate in still showed a Join lock and
"invite-only — you need an invite to post": `RelayGroupChannel.membershipOf()`
reads only the per-channel NIP-29 roster (39001/39002), and the UI treated the
`closed` metadata tag as invite-only.

But Buzz's write gate (buzz-relay `check_channel_membership`) is *member OR
visibility=="open"*: an open (non-`private`) channel accepts kind-9 from any
authenticated relay member with no per-channel join, and Buzz stamps `closed`
onto every channel — so `isClosed()` says nothing about who may post; only
`isPrivate()` reflects the write ACL.

Add `RelayGroupChannel.requiresMembershipToPost()` (Buzz open channels don't;
standard NIP-29 relays always do) + `canPost(pubkey)`, and route the composer,
threads FAB, top-bar Join button, and discovery Join button + invite-only badge
through it. Standard NIP-29 relays are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:59:47 -04:00
Claude e86983893c feat(buzz): sectioned community view — Channels, Forums, inline DMs, then Agent Console
Reworks the Buzz relay's Community view to mirror Buzz's own sidebar
ordering instead of stacking the DM + Console entries on top of the
channels:

- Channels and Forums are now separate sections, split by the relay-signed
  39000 `channel_type` (stream vs forum); DM-typed channels are excluded
  from both (they belong to the DM section). Adds isBuzzForum() and the
  forum/stream type constants alongside the existing DM reader.
- Direct Messages moves below the channels and renders inline: the most
  recent conversations (avatar + name + last-activity time), a New-message
  action in the section header, and a "See all N" row into the full inbox
  when there are more than fit.
- Agent Console drops to a single footer card at the very bottom.
- Section headers get a consistent modern style (primary-colored labels
  with optional trailing actions), replacing the old top-stacked action
  cards. BuzzImportRow gains an onOpen tap target so a channel row opens
  the chat while keeping its Add-to-list affordance.

Vanilla NIP-29 relays are unchanged (flat channel directory, no
forums/DMs/console).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-23 01:51:12 +00:00
Vitor PamplonaandGitHub 56f8968172 Merge pull request #3676 from vitorpamplona/claude/privacy-options-modernize-9l51qs
Refactor privacy settings UI to use composable components
2026-07-22 21:06:51 -04:00
Claude 7c4090a800 feat(settings): modernize Privacy, Profile UI, Home Tabs & Calendar Reminder screens
Bring four settings screens onto the SettingsSection card kit used by the
UI Preferences / Security Filters screens, and drop the last Save/Cancel
flow in favor of auto-save + back-button-only navigation.

- Privacy Options: remove SavingTopBar + TorDialogViewModel staging; bind
  controls directly to TorSettingsFlow.tryEmit (the existing debounced
  propertyWatchFlow already persists changes), so edits save on change and
  the screen has only a back arrow. Rebuild the body from SettingsSection
  cards: a segmented Tor-engine tile, an Orbot-port block, a live preset
  picker row, and per-usage switch tiles.
- Profile UI & Home Tabs: reskin the ad-hoc rows into SettingsSection
  cards with SettingsSwitchTile / SegmentedChoiceTile (Home Tabs keeps the
  "can't disable the last tab" guard via the enabled flag).
- Calendar Reminders: swap the hand-rolled TopAppBar for
  TopBarWithBackButton and reskin into a SettingsSection card with a
  segmented lead-time selector, preserving the CalendarReminderPrefs writes
  and WorkManager schedule/cancel side-effects.
- Shared kit: make SegmentedChoiceTile internal for reuse and add a compact
  title-only SettingsSwitchTile overload with enabled support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013JvrwLYbGVJQhabfZS7sBy
2026-07-23 00:57:09 +00:00
Claude e83fa73cc3 fix(buzz): warm-auth DM/console reads + reconnect-on-join to authenticate the socket, and user-search for New DM
The Buzz relay gates its `#p=me` reads (44100 member-added, 30622 DM
visibility) and the 39002 group roster behind NIP-42. Two gaps kept
those empty even after joining a workspace:

- DM list and Agent Console used a plain paged fetch, which returns
  empty on an `auth-required` CLOSED. Switch both to the warm-auth
  `fetchAllWithHooks(pendingOnAuthRequired = true)` path the import
  already used, so they authenticate on the CLOSED and retry.

- NIP-42 sends its AUTH challenge once, on connect. When the socket was
  already open before the user joined (the relay is in their lists and
  connected at startup), that challenge was spent while the relay was
  still not first-party, leaving the persistent group-roster subscription
  refused — so the channel showed a "Join" lock. A join makes the relay
  first-party (AuthCoordinator.isFirstParty); force a reconnect on a new
  join so the relay re-challenges and the connection authenticates,
  unlocking the roster and every other #p=me read on the shared socket.

Also reworks the New Buzz DM recipient picker: instead of only accepting
a raw npub/hex, it now offers a typeahead user search
(LocalCache.findUsersStartingWith) that surfaces this workspace's channel
members first. Pasting an npub/hex still works as an escape hatch for
someone not yet in the local cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-22 23:35:32 +00:00
Claude 94d5f55f93 Merge remote-tracking branch 'origin/main' into claude/buzz-repo-analysis-7k54ga 2026-07-22 22:58:51 +00:00
Claude e1f732ee38 refactor(buzz): nest DMs + Agent Console per-community, drop drawer entries
Buzz DMs and the Agent Console are community-scoped (a DM is a t=dm relay group on
one community's relay; agents/telemetry live on community relays), so they don't
belong as global drawer entries. Remove the AGENT_CONSOLE + BUZZ_DMS nav items and
reach both from within a community instead: the Buzz relay's group screen now shows
"Direct Messages" and "Agent Console" cards scoped to that one relay.

- Route.BuzzDmList / BuzzNewDm / AgentConsole now carry a relayUrl.
- BuzzDmListViewModel / BuzzNewDmViewModel / AgentConsoleViewModel take a single-relay
  scope (and mark it joined + pre-approve NIP-42 so the #p=me discovery authenticates).
- Removed the settings-catalog AgentConsole search entry (no longer global).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-22 22:58:41 +00:00
Vitor PamplonaandGitHub 7a9457e88b Merge pull request #3675 from vitorpamplona/claude/concordcommunitylistevent-relay-loading-s95nxm
Bootstrap Concord communities from pinned bottom-bar tabs
2026-07-22 18:52:09 -04:00
Claude 5166a5ac69 fix(concord): load pinned communities from their own relays
A Concord community pinned to the bottom bar wouldn't load at all when its
private kind-13302 joined-communities list wasn't already cached: the tab and
its server screen stayed blank. The list often lives only on the community's
own relays (Armada/Vector publish it there, never to the user's outbox), and
the only fetch that looked beyond the outbox — importConcordCommunities — was
triggered solely from the Concord hub and never queried the community's relays.

Carry each pinned community's bootstrap relays on its BottomBarEntry.Concord
tab (captured from the joined-list entry at pin time) and:

- importConcordCommunities now takes extra relays and folds in the relays saved
  on every pinned Concord tab, so the list is found where it actually lives;
- ConcordChannelPreload bootstraps app-wide: it fetches the list for any pinned
  community we don't yet know, so the tab and server screen fill in without the
  user ever opening the hub.

Once the list folds into the cache, ConcordChannelListState.liveCommunities
already surfaces it reactively (verified by a new late-arrival test) and the
plane preload picks the community up — so a late-arriving list with no local
backup now updates the tab and the Concord Channels screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSiSXaHMEpuo3gcDuTZ24u
2026-07-22 22:40:46 +00:00
Claude abdcd9c39d fix(relay): show relay groups when NIP-11 is unreachable; reconnect on trust change
Two fixes for the "connected over clearnet, receiving 39000s, but nothing shows"
case on a Cloudflare-fronted relay (blocks the plain HTTP NIP-11 GET while serving
events over the socket):

- Relay group-list screen no longer hard-depends on NIP-11. The display gate
  needs the relay's `self` key (from NIP-11) to show relay-signed groups; when the
  NIP-11 fetch fails (FAIL_TO_REACH_SERVER — Cloudflare resets the GET), fall back
  to the dominant 39000 signer on that relay as its de-facto signer, so its groups
  render while a stray user-published 39000 (different author) stays filtered. Also
  re-fetch NIP-11 when the relay is marked Trusted (moved to clearnet), busting the
  cached over-Tor error (new Nip11CachedRetriever.invalidate).

- RelayProxyClientConnector now reconnects the transport-flipped relay immediately
  when a relay-classification set changes (e.g. marking it Trusted → clearnet).
  Previously only TorRelaySettings changes counted, so a newly-trusted relay sat
  out its Tor-earned backoff before re-dialing. Scoped to onlyIfChanged (no
  resetBackoff), so only the flipped relay skips its delay and the rest of the
  pool's backoff is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
2026-07-22 22:35:06 +00:00