Commit Graph
482 Commits
Author SHA1 Message Date
Claude 3ed2245d8c feat: on-chain zap splits
Extends NIP-BC onchain zaps to honor a note's NIP-57 zap-split tags: one
Bitcoin transaction pays every pubkey-based recipient atomically, and
one kind:8333 receipt is published per recipient (each receipt carries
the recipient's pubkey + sat share and shares the same i:<txid>).

quartz / OnchainZapBuilder
- new buildSplit(recipients = listOf(pubkey to sats), ...) produces a
  PSBT with one output per recipient + optional change output
- existing build(...) now delegates to buildSplit; coin selection and
  change-vs-dust logic are unchanged for the single-recipient path

commons / new OnchainZapSplitter
- distribute(totalSats, splits, dustThreshold) does the weighted
  integer-math allocation, dropping the rounding remainder onto the
  largest-weight recipient first so the per-recipient sats sum exactly
  to totalSats
- throws DustRecipientException if any share lands below dust; the
  caller surfaces that as a build-stage failure before the tx is built
- unit tests cover equal weights, fractional weights, remainder
  distribution, dust rejection, and input-order preservation

commons / OnchainZapSender.sendSplit
- mirrors send() but takes the precomputed shares, builds via
  buildSplit, and publishes N receipts using the same txid; if one
  receipt publish fails the broadcast txid + already-published receipt
  ids are surfaced in the Failure result

amethyst / Account.sendOnchainZapWithSplits
- thin wrapper that hands off to OnchainZapSender.sendSplit using the
  signer's pubkey

amethyst / OnchainZapSendDialog
- detects pubkey-based zap splits on the zappedEvent and, when present,
  defaults to split mode: a SplitsRecipientSection renders one row per
  recipient with weight % and live per-recipient sats preview
- lnAddress-only splits are filtered out (no pubkey -> no Taproot
  address); a short note tells the user how many recipients were
  skipped
- the send button label switches to "Send X sats, N ways"; an opt-out
  button lets the user fall back to single-recipient mode
- on send: shares are recomputed via OnchainZapSplitter; below-dust
  configurations surface as a BUILDING-stage failure before signing
2026-05-20 19:55:15 +00:00
Vitor Pamplona 872bb4f245 Add new information icon 2026-05-20 14:44:59 -04:00
Vitor PamplonaandGitHub 99e98e0213 Merge pull request #3012 from vitorpamplona/claude/social-wallet-info-popup-4O6s6
Add public wallet warning chip and dialog to onchain section
2026-05-20 13:32:50 -04:00
Claude 7232456e9e fix: share payment targets dialog with reactions row, restore QR glyph
- Extract PaymentTargetsDialog from PaymentButton so the reactions row
  can render the same QR/Copy/Pay layout instead of the legacy two-row
  M3ActionRow per target.
- PayReaction now subscribes via EventFinderFilterAssemblerSubscription
  and observes the PaymentTargetsEvent reactively, so targets actually
  load when the wallet icon is tapped.
- Correct the QrCode2 codepoint (U+E00A) and re-subset the bundled
  Material Symbols Outlined font so the glyph is no longer a tofu box.

https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8
2026-05-20 16:53:11 +00:00
Claude fc8f057e3e feat: add Public chip to onchain wallet card
The onchain wallet's Taproot address is derived from the account's Nostr
pubkey, so anyone with the npub can see its balance and transaction
history on-chain. Surface that fact directly on the card with a tappable
"(i) Public" chip that opens a dialog explaining the privacy implication
and recommending private (non-Nostr) channels for funding and draining.
2026-05-20 16:39:34 +00:00
Claude ec28d4eb4c feat: redesign payment targets modal with QR, copy and pay buttons
Each target now renders in a single row with the payment method and
address on the left and three trailing icon buttons: QR (opens a dialog
showing the raw address as a QR code), Copy (clipboard + toast), and
Pay (existing payto:// intent). Adds QrCode2 to MaterialSymbols.

https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8
2026-05-20 16:35:47 +00:00
davotoula e131b0fec2 Show on chain zaps even if only reaction 2026-05-20 08:23:10 +02:00
Claude 6a0801427b Revert "Merge pull request #2990 from vitorpamplona/claude/add-i2p-privacy-option-nK2X7"
This reverts commit d42482ff56, reversing
changes made to a8b6766f49.
2026-05-19 23:10:56 +00:00
Vitor PamplonaandGitHub 679c5a89e0 Merge pull request #2994 from vitorpamplona/claude/nip52-calendar-screens-yQc3j
Add NIP-52 calendar events support with UI and reminder system
2026-05-19 18:30:59 -04:00
Claude c7a4796b25 refactor(marmot): audit follow-ups on MLS reply paths
Self-audit of the previous MLS reply commit surfaced four concrete
improvements:

1. Push-notification cold-start replies now thread reliably. The
   receiver previously rebuilt the parent inner event by looking it up
   in LocalCache; in a cold-process broadcast that cache hasn't been
   re-hydrated yet (Account.restoreAll runs async on init), so the
   q-tag silently dropped. Carry the parent's inner event id AND
   author pubkey through the Intent extras and feed them straight to
   MarmotManager.buildTextMessage, which now takes (eventId, author)
   instead of a full Event. The cold-start reply is now always
   threaded, not just the warm-cache case.

2. marmotGroupRelays() is no longer duplicated. The receiver was
   reimplementing what AccountViewModel had as a private fun (which
   itself was used 9× inside the VM). Lifted to Account so headless
   callers can reach it without spinning up a ViewModel; both sites
   now share one implementation.

3. Dropped the dead editFromDraft / draftId plumbing. Added for route
   symmetry with NIP-17 but Marmot has no draft persistence, so the
   parameter rode all the way through MarmotGroupChatView only to be
   ignored under @Suppress("UNUSED_PARAMETER"). Per CLAUDE.md, don't
   pre-emptively abstract; reinstate when drafts actually land.

4. MarmotGroupMessageComposer no longer has default-param `remember`
   blocks. There's exactly one caller and it always passes both
   messageState and replyTo — the defaults were just noise.

The in-chat send path also captures (id, pubKey) under the replyTo
guard before launching the send coroutine, so a slow send + a user-
cleared reply state can't race into a partially-threaded message.

No protocol or behavioral change for the warm-cache happy path; the
threading improvement is observable only on cold-start push-reply.
2026-05-19 22:04:14 +00:00
Claude 382729520c chore(commons): regenerate Material Symbols font subset to include EventAvailable
The "Add to phone calendar" icon I added on the calendar detail screen
top bar mapped to MaterialSymbols.EventAvailable (\\uE614), but the
checked-in TTF was a subset built before that codepoint existed in
MaterialSymbols.kt — so the third icon from the right rendered as the
.notdef placeholder.

Regenerated via tools/material-symbols-subset/subset.sh. The subset is
now 214 codepoints (up from 213); file size unchanged at 420K.
2026-05-19 21:58:35 +00:00
Claude ab5d884b34 feat(calendars): "Add to phone calendar" intent + multi-day bars in month grid
- New IconButton in the detail screen opens the system event composer
  (Google Calendar / Samsung / iCloud) pre-filled with title, range,
  location, and description via Intent.ACTION_INSERT on CalendarContract.
  Falls back to the .ics share path if no calendar app is installed.
- Replaced the per-cell event-dot row with horizontal bars that span the
  cells a multi-day event covers. Bars are laid out by a greedy
  lowest-lane assignment so overlapping events stack rather than collide;
  cells removed their horizontal padding so adjacent bars merge into one
  uninterrupted line.
- Bars round their ends only at the event's actual start/end (or at week
  boundaries), so a 3-day conference reads as one continuous pill across
  the row.
- Added MonthGridBarsTest (7 tests) covering single/multi-day, lane
  collision, longer-event tiebreak, and the empty/ghost edge cases.
2026-05-19 21:58:34 +00:00
Claude 2326738b84 fix(marmot): route reply button on MLS messages to the encrypted group
Tapping reply on a Marmot/MLS (kind:445) message in the Notifications
screen previously fell through routeReplyTo()'s `else` branch and opened
the generic public-comment composer (Route.GenericCommentPost). Sending
it would publish a plaintext kind:1111 that references the encrypted
inner event id, leaking that the user has decrypted that group message.

Mirror the NIP-17 pattern (Route.Room carries replyId, draftId,
draftMessage; the same chat screen renders the "replying to" quote
above the input) for MLS:

- routeReplyTo() now detects MarmotGroupChatroom in note.inGatherers,
  matching how routeFor() at line 67 already finds the parent group, and
  returns Route.MarmotGroupChat(groupId, replyId = note.idHex).
- Route.MarmotGroupChat gains message/replyId/draftId fields.
- MarmotGroupChatView resolves the replyId into a Note, shows
  DisplayReplyingToNote above the composer, wires onWantsToReply for
  in-chat replies, and threads the parent inner event through
  AccountViewModel.sendMarmotGroupMessage into
  MarmotManager.buildTextMessage, which now adds a NIP-18 q-tag on the
  inner kind:9 (the same convention ChatEvent.reply() uses).

Push notifications: notifyGroupMessage previously passed
chatroomMembers=null, so the inline Reply action was never attached for
MLS group notifications. Add a parallel MARMOT_REPLY_ACTION wired with
the group id + parent inner event id; NotificationReplyReceiver loads
the account, rebuilds the parent inner event from LocalCache (or sends
unthreaded if the cache was pruned), and publishes the reply through
the same MarmotManager path — so the inline notification reply stays
encrypted inside the group instead of taking the NIP-17 PTag fallback.
2026-05-19 21:45:53 +00:00
Vitor PamplonaandGitHub d42482ff56 Merge pull request #2990 from vitorpamplona/claude/add-i2p-privacy-option-nK2X7
Add I2P support with unified privacy routing
2026-05-19 16:56:54 -04:00
Claude 40995b9670 refactor(privacy): drop I2pType.INTERNAL — EXTERNAL is the permanent answer
Earlier commits kept I2pType.INTERNAL as a placeholder for a follow-up
embedded daemon. We're not shipping that: I2P bootstrap on Android is
structurally minutes-long (no equivalent of Tor's hardcoded directory
authorities — NetDB peer discovery is protocol-inherent), so an embedded
router would mean a permanently-warming UX. Users who want I2P run i2pd /
Java I2P independently and point Amethyst at its SOCKS port.

Changes:
- commons I2pType drops the INTERNAL variant; parseI2pType collapses unknown
  codes to OFF
- I2pManager loses its INTERNAL switch arm
- I2pSettingsDialog drops the "treat INTERNAL as OFF" mapping it used to
  carry — the enum no longer has the case
- Android UI I2pSettings.resourceId drops the i2p_internal branch
- strings.xml drops the now-unused i2p_internal string
- I2pSharedPreferences hardens load: a stored "INTERNAL" from an earlier
  branch is no longer a valid I2pType, so runCatching swallows the
  IllegalArgumentException and the user lands on OFF
- PrivacyRouterTest fixtures use I2pType.EXTERNAL throughout
2026-05-19 20:37:53 +00:00
davotoula cf44c092dd Code review:
- invalidate zaps flow when removeAllChildNotes clears onchainZaps
- simplify on-chain zap gallery after review
2026-05-19 13:10:02 +02:00
davotoula 91ded74636 Show on-chain zappers in expanded reactions gallery
- Add OnchainZappedIcon and PendingClockBadge
- move PendingClockBadge to TopStart to avoid follow-dot clash
2026-05-19 13:10:02 +02:00
Vitor PamplonaandGitHub 0d91245e02 Merge pull request #2971 from vitorpamplona/claude/add-nowhere-links-LPMnM
Add Nowhere link detection and branded card rendering
2026-05-18 19:20:34 -04:00
Claude b16d44721c perf(richtext): collapse nowhere-link classifier into the existing URL branches 2026-05-18 22:54:35 +00:00
Vitor PamplonaandGitHub 34f5dcac69 Merge pull request #2959 from greenart7c3/claude/fix-blossom-cache-url-8zolt
fix(blossom): only bridge to local cache when URL is BUD-01 layout
2026-05-18 08:49:22 -04:00
Claude 09c4d70048 fix(blossom): only bridge to local cache when URL is BUD-01 layout
`bridgeUrl` accepted the imeta `x` hash as a sufficient signal to route
a media URL through the local Blossom cache, even when the URL itself
didn't have the sha256 in its last path segment. For URLs like
https://i.nostr.build/M5AwJ.gif the upstream serves the blob under an
opaque filename, so the resulting `xs=https://i.nostr.build` hint sent
the local cache after https://i.nostr.build/<sha>.gif on miss — which
404s because the real blob isn't at that path.

Match the behaviour `bridgeProfilePictureUrl` already had: require
`extractSha256FromUrlPath(url)` to succeed before bridging. The imeta
hash is still preferred for canonical casing when present, but is no
longer enough on its own.
2026-05-16 22:45:06 +00:00
Claude 4a23ecf7ef Raises the default maximum hashtag limit from 5 to 8 2026-05-16 21:38:58 +00:00
Claude 318e4250de feat: render nowhere links inline as branded cards
Nowhere (https://github.com/5t34k/nowhere) encodes whole mini-sites in
a URL fragment, so the OpenGraph preview path returns nothing useful and
hostednowhere.com 403s scrapers. Detect URLs on hostednowhere.com and
nowhr.xyz that carry a fragment as a NowhereLinkSegment and render them
through a NowhereLinkCard that labels the tool type (Event, Store,
Message, ...) and opens the URL in the browser on click.
2026-05-16 20:35:37 +00:00
Claude a011421ff5 refactor(privacy): single preferred clearnet transport, fail-closed hidden services
Drop the per-feature 3-way picker — no splitting clearnet traffic between Tor
and I2P at the same time. Both daemons can still run side-by-side so .onion
and .i2p hidden services stay reachable independently, but only one transport
carries clearnet traffic at a time.

Routing model:
- .onion → Tor required; Blocked when Tor is OFF (no clearnet fallback)
- .i2p   → I2P required; Blocked when I2P is OFF (no clearnet fallback)
- clearnet → preferredClearnetTransport (NONE/TOR/I2P) picks the active
  transport; that transport's own per-feature toggle decides this request;
  otherwise Direct

Commons:
- Add PrivacyRoute sealed type { Direct, Tor, I2p, Blocked(BlockReason) } so
  fail-closed has somewhere to land — callers must surface Blocked instead of
  silently leaking over clearnet
- PrivacySettings drops `features`, adds preferredClearnetTransport
- I2pSettings gains imagesViaI2p / videosViaI2p / urlPreviewsViaI2p /
  profilePicsViaI2p / nip05VerificationsViaI2p / moneyOperationsViaI2p /
  mediaUploadsViaI2p — mirrors TorSettings; only effective when I2P is the
  preferred clearnet transport
- PrivacyRouter rewritten to the new model
- Delete FeatureTransportChoices and TransportChoice
- Keep FeatureRole as the per-request hint that selects which toggle to read

Tests: PrivacyRouterTest rewritten to cover the new outcomes, including
both-daemons-running clearnet preference, fail-closed for .onion / .i2p, and
that the non-preferred transport's toggles have no effect on clearnet.
2026-05-16 20:06:35 +00:00
Claude fb2f05d9cb feat: scaffold I2P as a parallel privacy transport to Tor
Foundational types for offering I2P alongside the existing internal Tor.
No wiring yet — HTTP managers, RoleBasedHttpClientBuilder, the Android
I2P service and the Privacy settings UI follow in later commits.

Quartz (relay URL classifier):
- Add isI2p() and classifyHidden() to RelayUrlNormalizer
- Add HiddenServiceKind { CLEARNET, LOCALHOST, ONION, I2P }
- Add NormalizedRelayUrl.isI2p() / classifyHidden() extensions
- Extend the scheme-default branch so .i2p hosts default to ws:// like .onion

Commons (transport-agnostic types):
- PrivacyTransport enum { DIRECT, TOR, I2P }
- TransportChoice (UI-facing per-feature picker, screen-coded for persistence)
- FeatureRole + FeatureTransportChoices: per-feature picks for clearnet traffic
- PrivacySettings aggregate { tor, i2p, features }
- PrivacyRouter.route(url, role, settings): hostname pin for hidden services,
  per-feature choice for clearnet, downgrades to DIRECT if backing transport is OFF

Commons (I2P settings model, mirrors tor/):
- I2pSettings, I2pType (OFF/INTERNAL/EXTERNAL), I2pRelaySettings
- I2pRelayEvaluation, I2pServiceStatus
- II2pManager, II2pSettingsPersistence (platform-agnostic interfaces)
- PrivacyRelayEvaluation composing TorRelayEvaluation + I2pRelayEvaluation

Tests:
- PrivacyRouterTest covers localhost bypass, onion pin, i2p pin, hostname-wins-over-picker,
  per-feature picks routing independently, downgrade-when-transport-OFF, .b32.i2p
2026-05-16 19:37:06 +00:00
Claude 8a5d0019c1 Merge remote-tracking branch 'origin/main' into claude/nip88-polls-quartz-p5fBa 2026-05-16 19:15:32 +00:00
Claude 62d97c6d82 fix(blossom): require sha256 at start of last path segment
URLs like https://nostr.build/i/nostr.build_<sha>.jpg embed a 64-char
hex in the filename but aren't BUD-01 Blossom blobs — the last path
segment must be exactly <sha256> or <sha256>.<ext>. The previous regex
matched the embedded hash and rewrote the request to the local cache
with xs=https://nostr.build/i, which 404s on miss because the real blob
lives at /i/nostr.build_<sha>.jpg, not /i/<sha>.

Switch both extraction sites (MediaUrlContentExt and the OkHttp
interceptor) to a matchEntire regex that anchors the sha at the start
of the segment with at most a .<ext> suffix.
2026-05-15 22:32:01 +00:00
davotoula e79755dab3 fix(nip05): stop reporting verification errors as Verified 2026-05-15 17:17:43 +02:00
Claude a690777ef6 feat: show external-id scope as reply context for NIP-22 comments
A Comment event (kind 1111) scoped to an external identifier (`I` tag)
previously rendered as a bare text post and landed in the Home "New
Threads" feed. Treat it as a reply to that external scope: it now shows
in the conversations feed and renders a typed chip (hashtag, geohash,
url, or generic) above the comment text.

https://claude.ai/code/session_01ArHkNXu1ANrVGZAyMWg4Xu
2026-05-14 15:05:04 +00:00
Claude db6254872e fix(onchain-zaps): recover the OnchainZapBuilder package + re-audit fixes
The big one: the source package was named `nipBCOnchainZaps/build/`, which
the repo .gitignore (`build/`) silently matched — so OnchainZapBuilder.kt
(the main source, not just its test) was NEVER committed. The pushed branch
did not compile; the pre-commit hook only checks the working tree, so this
went unnoticed. Renamed the package `build` -> `builder` (a source package
must never be named `build`) and updated all references; the recovered files
are now actually tracked.

Re-audit findings on the previous fix commit:
- CachingOnchainBackend.txCache was unbounded -> memory leak in a long
  session. Added a bounded cache (maxCachedTxs, oldest-entry eviction).
- OnchainZapSender still trusted the signer's returned PSBT for witness
  UTXOs and tap internal keys (used to compute the sighash + the verified
  output keys). Now it copies ONLY the PSBT_IN_TAP_KEY_SIG records back onto
  the PSBT we built, so a signer can contribute signatures and nothing else;
  verification and finalization run entirely on our own PSBT.

Test coverage added:
- OnchainZapBuilderTest: confirmed-UTXO filter — unconfirmed UTXOs excluded
  by default, spendable only with allowUnconfirmed, confirmed preferred.
- CachingOnchainBackendTest: confirmed-tx cached forever, unconfirmed
  re-fetched after TTL, not-found never cached, tip/fee TTL, bounded
  eviction.

All quartz / commons jvmTest suites pass; quartz android + amethyst compile.
2026-05-14 13:55:18 +00:00
Claude 1d7be1d444 fix(onchain-zaps): address audit findings — fund-safety, interop, robustness
CRITICAL
- PsbtSignatureVerifier: independently verifies every key-path signature in a
  PSBT (BIP-340 sig over the BIP-341 sighash, against the tweaked output key).
- OnchainZapSender now rejects a signer that returns a different transaction
  than it was asked to sign (byte-compares the unsigned tx) and verifies all
  signatures before broadcasting — closes a substitution attack where a
  malicious/buggy external signer could redirect funds.

HIGH — break test circularity
- Pin the full BIP-341 keyPathSpending input-0 witness: signing the vector
  sighash with the vector tweaked key reproduces the vector signature
  byte-for-byte (also pins BIP-340 nonce determinism).
- Pin TaprootAddress.fromPubKey + SegwitAddress.encodeP2TR against all seven
  BIP-341 wallet-test-vector P2TR mainnet addresses.
- EsploraBackendTest: JSON-parsing coverage for /tx, /address/{}/utxo, the
  mempool.space and blockstream fee formats, and schema-fallback.

MEDIUM
- OnchainZapBuilder filters to confirmed UTXOs by default (allowUnconfirmed
  opt-in) and signals BIP-125 RBF (nSequence 0xFFFFFFFD).
- CachingOnchainBackend: TTL-caching decorator (confirmed tx forever,
  unconfirmed/tip/fees short TTL) so a feed of onchain zaps doesn't fan out
  into one HTTP request per event. Wired in AppModules.
- OnchainZapVerifier computes real confirmation depth from the chain tip and
  asserts the backend echoed the requested txid.
- EsploraBackend falls back to the standard Esplora /fee-estimates endpoint
  (blockstream.info) when /v1/fees/recommended 404s.

LOW
- BitcoinTransaction.parse caps input/output/witness-item counts to stop a
  hostile varint from triggering a giant pre-allocation.
- OnchainZapEventTest: asserts kind:8333 on-the-wire tag structure against the
  NIP-BC spec.
- alt tag now includes the amount ("Onchain zap: N sats"), matching the spec
  example.

All quartz / commons / androidHostTest suites pass; amethyst compiles.
2026-05-14 13:29:48 +00:00
Claude f45479615e Merge remote-tracking branch 'origin/main' into claude/add-zap-button-nest-KoZG4
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt
2026-05-14 13:08:13 +00:00
Claude 524bc90d5a test(nests): cover RoomZapsAggregator; tidy zap-overlay comments
Adds RoomZapsStateTest mirroring RoomReactionsStateTest (grouping,
eviction, room-wide keying, idempotent snapshots, dedup). Also drops
the unused RoomZapsAggregator.isEmpty(), pins the zap chip's content
color to white since BitcoinOrange is theme-independent, and corrects
a few doc comments that overstated component/timer sharing.

https://claude.ai/code/session_01EW11kUdiEYPuPti7vtD2AR
2026-05-14 12:49:18 +00:00
Vitor PamplonaandClaude Opus 4.7 78be534df5 nests room: stable reactions overlay + repeatable kind-7 + speaker controls
Reaction-side rework after extended testing on 2026-05-13:

* Promoted users weren't seeing speaker controls in their own
  Amethyst — `StageControlsBar` gates on `isOnStage && ui.onStageNow`
  and `ui.onStageNow` was stuck at false (never reset back to true
  after some earlier path flipped it). Added a LaunchedEffect in
  NestActivityContent that mirrors `isOnStageMe → ui.onStageNow`
  whenever it becomes true, symmetric to the auto-stop effect.

* Reactions overlay sat inside AvatarAndBadges' wrap-content Box,
  so adding/removing the chip shifted the inner Box's centre and
  the role badges drifted. Lifted the overlay out to be a sibling
  of AvatarAndBadges inside the outer fixed-size Box; badges now
  stay anchored regardless of chip presence or animation state.

* Reaction grouping was by `targetPubkey` (NIP-25 p-tag) — emojis
  showed on the speaker being reacted to, not the reactor. Switched
  RoomReactionsAggregator to group by `sourcePubkey`. AudienceGrid
  threads `reactionsByPubkey` so an audience reactor sees their own
  emoji float from their audience-tab avatar too.

* Reaction chip animation: progress was an `Animatable.value` that
  Compose wasn't refreshing in the layout-consuming layer (visible
  bug: chip "blinked" with no movement). Replaced with a manual
  `withFrameNanos` loop writing to a `MutableFloatState` read inside
  a `graphicsLayer { … }` lambda — frame-clock animation that works.
  Each kind-7 is its own chip keyed by event-id (no more
  groupBy-content collapsing same-emoji bursts into one shared chip
  that restarts on every arrival). Chips stack at a fixed-size 30 dp
  Box with the emoji centred, so the X position is invariant under
  glyph width. Multiple concurrent chips overlap at the right-bottom
  corner in a `Box(BottomEnd)` (newest on top) rather than sliding
  leftward in a `Row`.

* Bottom-drawer `RoomReactionPickerSheet` (hard-coded 6 emojis, no
  NIP-30) replaced by a forked `RoomReactionPopup` that reuses
  `ReactionChoicePopupContent` (same NIP-30 custom-emoji support,
  same user-configured reaction set) but with two semantic deviations
  for live audio rooms: empty `toRemove` so all buttons stay
  "fresh", and the click handler signs+broadcasts a fresh kind-7
  template directly instead of going through `Account.reactTo` —
  which delegates to `ReactionAction.reactTo(note, …)` and
  short-circuits on `note.hasReacted(by, reaction)`. The bypass
  lets the user fire the same emoji repeatedly during a moment.

* Chip rendering matches NoteCompose: `RenderReactionContent`
  handles `:shortcode:url` via `InLineIconRenderer` + `CustomEmoji`,
  `"+"`→❤️, `"-"`→👎, anything else as Text.

Tests updated for sender-grouped aggregator semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:45:21 -04:00
Vitor PamplonaandClaude Opus 4.7 d21eb0e226 fix nests room host UX: visible toasts, promote propagation, edit-sheet keyboard, reaction animation
This commit bundles several issues surfaced while testing host actions
inside an audio room.

NestActivity didn't mount `DisplayErrorMessages`, so every toast emitted
by nest code (promote, demote, kick, force-mute, errors) queued into a
StateFlow whose only collector lives in MainActivity. Mounted it inside
NestActivity.setContent so toasts now render in front of the room UI —
same way the leave-confirmation AlertDialog already does.

Host actions used to fire a synchronous "Promoted X" toast regardless of
whether `signAndComputeBroadcast` actually completed. Silent signer
failures (TimedOut, ManuallyUnauthorized, CouldNotPerform, etc. — all
Log.w-only in launchSigner) were invisible from the host's POV. Replaced
with a coroutine-bound failure toast that surfaces the exception class
+ message; success is implicit via the UI update.

The real cause of "promoted user stays in audience tab" turned out to be
stale presence: a kind-10312 emitted before the role grant (with
onstage=0) was pinning the freshly-promoted speaker to the audience tab.
buildParticipantGrid now takes a `roleGrantSec` parameter (the
kind-30312 created_at) and treats presence as authoritative only when
strictly newer. Pinned with two new tests covering both the
stale-ignore and fresh-respect cases. The leave-stage-on-another-client
flow keeps working because that emits a fresh onstage=0.

RoomParticipantActions.rebuild now uses
`(original.createdAt + 1L).coerceAtLeast(now())` so a same-second
promote→demote can't tie-break the wrong way under NIP-01's lowest-id
rule.

EditNestSheet's bottom row got squashed when the keyboard appeared —
the form fields couldn't shrink, so the Save/Cancel/Close row took the
hit. Split into a scrollable form column + sticky action row, wrapped
the outer column in imePadding.

SpeakerReactionOverlay was driving drift on a 100 ms `delay` loop over
the 10 s eviction window — produced 0.16 dp per tick (visibly stepped)
and the chip barely moved before disappearing. Replaced with
`Animatable.animateTo` on Compose's frame clock and a 6 s duration so
the chip pops, lingers visibly, then drifts up and fades.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:45:20 -04:00
Claude 40ccffee35 feat: NIP-BC onchain zap send flow (Phase D)
Wires the Phase A.2 quartz send-side foundation into a working end-to-end
send: build → sign → broadcast → publish kind:8333.

commons/onchain/OnchainZapSender — stateless orchestrator. Loads the
sender's UTXOs, builds the unsigned PSBT via OnchainZapBuilder, signs it
through NostrSigner.signPsbt, finalizes + broadcasts via OnchainBackend,
then publishes the kind:8333 receipt through an injected publish callback.
Per-stage failures surface as OnchainZapSendResult.Failure (with the
broadcast txid preserved when only the receipt publish failed) instead of
throwing; CancellationException always propagates.

Account.sendOnchainZap — thin wrapper binding the account's signer, the
LocalCache.onchainBackend, and signAndComputeBroadcast into the
orchestrator.

amethyst wallet UI:
- OnchainZapSendDialog — collects recipient npub (or a fixed recipient when
  launched from a note's zap menu), amount, fee tier (slow/normal/fast from
  the backend's fee estimates), and an optional comment; runs the send and
  shows progress + success/failure.
- OnchainSection — the Bitcoin card on the wallet screen gains a "Send"
  button next to "Copy address" that opens the dialog for a profile zap.

Tests: OnchainZapSenderTest covers the happy path (receipt references the
broadcast txid, recipient, and amount), insufficient-funds failure at the
build stage, broadcast failure (no txid leaked), and publish failure (txid
preserved for retry).

Pending: Phase B (NIP-55 sign_psbt Intent) — external/remote signers still
throw UnsupportedMethodException; note-zap-menu entry point can reuse
OnchainZapSendDialog's recipientPubKey/zappedEvent parameters once wired.
2026-05-14 12:29:31 +00:00
Claude 773d61692e feat: add audio playback support for .f4a files
F4A is AAC audio in an MP4 container (Adobe's variant of .m4a).
Register the extension in the media URL list so it routes to the
player, and map it to audio/mp4 so ExoPlayer picks the MP4 extractor.

https://claude.ai/code/session_01EuB46tfwBxtvRNDz9Ju99J
2026-05-14 10:56:45 +00:00
Claude 031159b3f6 feat(amethyst): NIP-BC onchain zap receive + display
Phase C of the onchain zaps plan (amethyst/plans/2026-05-14-onchain-zaps.md).
Plumbs the Quartz receive foundation from Phase A.1 into the Android app so
incoming kind 8333 zaps show up in note totals and the wallet screen.

Subscription / fetch path (kind 8333 rides existing zap filters, no new
assemblers per the plan):
- FilterRepliesAndReactionsToNotes: OnchainZapEvent.KIND in
  RepliesAndReactionsKinds (#e on notes)
- FilterUserProfileZapReceived: OnchainZapEvent.KIND in
  UserProfileZapReceiverKinds (#p on profiles)
- UserProfileZapsViewModel: kinds = LnZap + Onchain inline
- FilterNotificationsToPubkey: OnchainZapEvent.KIND in SummaryKinds
- NotificationFeedFilter / NotificationDispatcher: kind 8333 in
  NOTIFICATION_KINDS
- FilterMessagesToLiveStream / FilterGoalForLiveActivity: kind 8333
  alongside 9735

Display path (Note.zapsAmount fold-in — no UI changes needed downstream):
- commons/model/Note.kt: new `onchainZaps: Map<String, OnchainZapAmount>`
  (keyed by txid for `(txid, target)` dedup); `addOnchainZap(txid, sats,
  confirmed)` mutator; `updateZapTotal` now also sums confirmed onchain
  sats into `zapsAmount`. Pending entries are tracked but excluded from
  the aggregate total per the NIP-BC spec.
- commons/model/OnchainZapAmount.kt: new data class for per-tx state.
- LocalCache: new `onchainBackend: OnchainBackend?` slot; new
  `consume(OnchainZapEvent)` handler that runs the same sig-verify and
  computeReplyTo pipeline as `consume(LnZapEvent)`, then kicks off async
  verification on `applicationIOScope`. Self-zaps are rejected
  synchronously per the spec. Verification failures, mempool-only txs,
  and confirmed txs all route to `Note.addOnchainZap` with the correct
  flag. Without a backend wired, events are still cached (so subscriptions
  see them) but skip the total contribution.
- LocalCache.computeReplyTo: new `is OnchainZapEvent ->` branch that
  enumerates `e`/`a` targets — profile-only zaps return an empty list and
  flow through profile zap queries instead.

Wallet UI:
- Wallet screen now renders a "Bitcoin" Card above the existing NIP-47
  NWC wallet list. Single card (one onchain wallet per Nostr identity,
  derived from the pubkey). Shows the bc1p taproot address + Copy button.
  Balance, recent incoming zaps, and tap-to-detail come in later phases.

Settings + wiring:
- AccountSettings: new `onchainEsploraEndpoint: MutableStateFlow<String>`,
  default mempool.space, plumbed for a future settings UI.
- AppModules: builds a single shared `EsploraBackend` at app init using
  the role-based money-flavored OkHttp client (Tor-aware), installs it on
  `LocalCache.onchainBackend` so verification can run.

Secp256k1Instance: new `pubKeyTweakAdd` was added in Phase A.1.

No PSBT, no signing, no broadcasting — send-side (Phase A.2 + D) is still
pending.
2026-05-14 04:54:40 +00:00
Claude 6425fcd651 feat(nests): add zap button to the full-screen action bar
Subscribes to kind-9735 zap receipts tagged with the room's a-pointer,
feeds them into the nest chat ledger (so RenderChatZap surfaces them
the same way live streams do) and into a sliding-window aggregator
that drives a floating  chip over the targeted participant's avatar
— mirrors the existing reaction overlay's animation cadence so both
streams visually feel like one system.

The button itself wraps NoteCompose's zapClick / ZapAmountChoicePopup
/ ZapCustomDialog defaults against the room's AddressableNote, so the
amount-choice popup, custom-amount dialog and multi-payable routing
all behave like the standard note  button.

https://claude.ai/code/session_01EW11kUdiEYPuPti7vtD2AR
2026-05-14 04:43:25 +00:00
Claude a12d5d147d chore: regenerate MaterialSymbols font subset for Remove glyph
The Stepper introduced in this branch references MaterialSymbols.Remove
(U+E15B). Regenerated via tools/material-symbols-subset/subset.sh so the
shipped TTF actually contains the glyph.
2026-05-13 23:36:29 +00:00
Claude 2816ceb479 feat: split Security Filters into a hub with per-category screens
Replaces the cramped single-screen layout (header settings + four tabs)
with a hub that uses the same grouped-card pattern as AllSettingsScreen,
plus a dedicated screen per blocked-content category. The tile control
mix is also modernized:

- "Show sensitive content" is now a 3-way SegmentedButton instead of a
  free-floating spinner sitting awkwardly next to switches.
- Numeric prefs (report threshold, max hashtags) use a stepper (- / + )
  instead of free OutlinedTextFields with no commit affordance.
- The report-threshold tile renders inline under its toggle and dims
  when the toggle is off, instead of appearing/disappearing and shifting
  layout.
- Each tile is a row with leading icon, title, description, trailing
  control - so the spinner/switch/stepper visual rhythm stays consistent.

The hub's "Blocked content" section shows a count badge per category and
routes to its own screen (Routes.BlockedUsers, SpammingUsers, HiddenWords,
MutedThreads). The hidden-words screen now docks the "Add word" field as
a bottom bar so it doesn't compete with the list for vertical space, and
selection mode is scoped per-screen instead of shared across tabs.
2026-05-13 20:48:53 +00:00
Vitor Pamplona 20a2270434 fix nests room author shown in audience with no audio
nostrnests publishes kind-30312 without a self-`p`-tag for the room
author — they're the implicit host. The lobby card already handles
this (NestJoinCard) but the in-room code did not: the author fell
into the pure-audience branch with role=null AND was missing from
the audio subscription set, so the host appeared as a regular
listener and no sound played.

buildParticipantGrid now takes hostPubkey and synthesizes a virtual
`["p", host, "", "host"]` when absent, so the existing presence-aware
onstage/audience rules apply uniformly — including "host leaves
stage" (onstage=0 → drops to audience, role stays HOST).

NestActivityContent owns the grid now and derives onStageKeys from
it, so the StageGrid render and the MoQ subscription set stay in
lockstep when anyone (host or speaker) steps off stage.
2026-05-13 15:41:51 -04:00
Vitor PamplonaandGitHub 16e87cea8d Merge pull request #2862 from davotoula/feat/161-mute-thread
Client-side thread mute (NIP-51 kind-10000 e tags)
2026-05-13 08:14:50 -04:00
Claude 3cc9ac8b7c fix(blossom-bridge): only the last path segment is the blob hash
BUD-01 defines a Blossom URL as `<server>/<sha256>[.<ext>]` — the blob hash is
always the last path segment. Walking the path right-to-left for any hex
match was too permissive: a non-Blossom URL like `https://example.com/<sha>/avatar.jpg`
(sha appears in an intermediate segment) would get incorrectly bridged.

Both parsers now look at the last path segment only and skip the URL entirely
when it isn't a sha256. The earlier hex-prefix case (share.yabu.me's
`<cache-prefix>/<blob>.ext`) still works because the blob is still the last
segment; the prefix flows into `xs` via the existing `buildServerBase` /
`extractServerBase` logic. Adds negative tests covering the
sha-in-non-last-segment case in both modules.
2026-05-13 10:27:26 +00:00
Claude 1b287baaf2 fix(blossom-bridge): pick rightmost sha256 segment in URL path
CDNs like share.yabu.me serve blobs under `<cache-prefix-sha>/<blob-sha>.<ext>`
where both path segments are 64-char hex. The bridge previously locked onto the
first match (the cache prefix), dropping the blob segment from `xs` and asking
the local cache for a non-existent blob. Walking the path right-to-left makes
the rightmost sha — the one that carries the file extension — win, while the
prefix segments stay in `xs` so the cache can fetch upstream on miss.

Behaviour is unchanged when the path has a single sha; tests cover the new
two-hash layout in both the Coil-model path and the OkHttp interceptor path.
2026-05-13 10:11:32 +00:00
davotoula ff780bcb54 Code review:
- consolidate thread-root resolution
- consolidate mute-state writes
2026-05-13 09:06:53 +02:00
davotoula bac3710deb Commons:
- Note.isHiddenFor drops notes in muted threads
- LiveHiddenUsers carries mutedThreads + isThreadMuted predicate
2026-05-13 09:06:53 +02:00
Vitor PamplonaandGitHub 925d9de71b Merge pull request #2857 from vitorpamplona/claude/fix-notecompose-scroll-jitter-8lw3R
Optimize Compose state management and flow subscriptions
2026-05-12 09:24:25 -04:00
Claude 4d9479fa1b Revert "refactor(RichTextViewer): move isMarkdown onto RichTextViewerState"
This reverts commit 7e2e3304e1.
2026-05-12 01:09:05 +00:00
Claudeanddavotoula 5df0e2a37a Add LAN video casting (Chromecast on play, DLNA on both flavors) v1
fix(cast): handle every Chromecast session terminal state and HLS mime hint
fix(cast): repair DLNA Res ctor and add play-only protocol toggle
fix(cast): backfill Cast button for accounts with pre-existing video settings
fix(cast): include cast glyphs in symbol font subset and add diagnostic logs
fix(cast): wire jUPnP's required Jetty deps and survive dialog dismissal
fix(cast): add jetty-client so jUPnP can issue UPnP control requests
2026-05-11 16:35:47 +02:00