Commit Graph
15076 Commits
Author SHA1 Message Date
Claude 23605ef630 feat(profile): 'Pay from' wallet selector on the Send Payment screen
The screen now shows which wallet the payment will come from and lets
the user switch before paying:

- Lightning and CLINK-offer rails list every configured wallet (NWC +
  CLINK debit, via PaymentSourceResolver.all) plus an 'Another wallet
  app' entry that hands the invoice to the system via intent. The
  selection defaults to the account's default payment source and
  re-resolves if the picked wallet is removed while the screen is open.
- On-chain and cashu rails show a fixed, disabled chip naming their
  intrinsic wallet so the money's origin is always visible.
- payBolt11 now charges the picked source instead of silently using the
  account default.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 21:18:11 +00:00
Claude 02e0d9a4be feat(profile): pay bitcoin payment targets through the in-app on-chain wallet
Lightning payment targets already route into the Send Payment screen;
this extends the same treatment to bitcoin targets. Tapping a profile's
bitcoin payment-target chip (or its pay action in the wallet-button
dialog) now opens the Send Payment screen with the on-chain rail locked
to that announced address, paid directly from the user's NIP-BC Taproot
wallet — falling back to the external bitcoin: URI when the chain
backend is missing or the address isn't a payable native-segwit mainnet
address.

- quartz: SegwitAddress.scriptPubKeyFor/isPayableMainnetAddress;
  OnchainZapBuilder.buildToScripts core shared by the pubkey paths.
- commons: OnchainZapSender.sendToAddress — plain wallet send with the
  same fund-safety signing contract but no kind:8333 receipt (the
  destination isn't pubkey-derived, so none is possible); the signing
  block is now a single shared helper across send/sendSplit/sendToAddress
  and Success.receiptEventId is nullable for receipt-less sends.
- amethyst: Account.sendOnchainToAddress; Route.SendPayment gains
  btcAddressOverride; a shared inAppPaymentRouteFor() decides which
  payment targets the user's wallets can pay in-app (used by both the
  target chips and the payment-targets dialog).
- Send Payment screen: with an address override the on-chain rail shows
  the target address, hides the message field (no receipt to carry it),
  explains that no zap receipt is published, and dispatches the plain
  address send.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 21:01:07 +00:00
Claude fa25f22f5f Merge remote-tracking branch 'origin/main' into claude/beautiful-ride-n6y3s2 2026-06-11 20:54:50 +00:00
Claude 1bb48e25df feat: broadcast private rumors as their delivering gift wrap
Brings DM-style broadcast to kind-1 private replies. Kind-14 chats carry
the kind-1059 host pointer on the event (WrappedEvent); other rumor
kinds can't, so LocalCache gains a rumorHosts index (rumor id → wrap
HostStub) populated when seals are unsealed and on seal replays after a
cache rebuild.

- Account.rumorHost(event): host from the event (WrappedEvent) or the
  index (other rumor kinds)
- Account.broadcast: any rumor with a known host re-downloads the wrap
  by id and republishes it — the unsigned rumor itself is never sent;
  rumors with no known wrap stay non-broadcastable
- Broadcast menu rows reappear for rumors when the wrap is known
  (AccountViewModel.canBroadcast), restoring the DM behavior the
  earlier blanket isPrivateRumor gate had also hidden for kind-14s

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:54 +00:00
Claude dd9ee0b5c6 fix: close audit findings — report leak, model-level rumor guards, hide share/bookmarks on private notes
Merge-readiness audit follow-ups:

- Account.report(note): reporting a private rumor now reports the AUTHOR
  (p-tag only) instead of publishing a kind-1984 that e-tags the private
  rumor id onto public relays (the one confirmed leak)
- Defense-in-depth guards at the model layer so the invariant no longer
  relies on UI gating alone: RepostAction.repost returns null / throws
  for empty-sig targets (covers Account.boost, createBoostEvent, and the
  desktop call path), ReactionAction.reactTo (simple overload) throws,
  and Account.broadcast no-ops for unsigned non-wrapped events — without
  the guard it would disclose the rumor JSON to relays even though they
  reject the signature
- Hide remaining actions that can't work on private rumors, per review:
  share buttons (action row, both note menus) and all bookmark/playlist/
  emoji-list rows (their lists reference an id other devices can't
  resolve; public lists would also leak it)
- ZapCustomDialog: remember(accountViewModel, baseNote) so the
  preselected zap type can't go stale on lazy-list slot reuse

Broadcast of the gift wrap itself (like DMs do via WrappedEvent.host)
needs host tracking for non-WrappedEvent rumor kinds in quartz — left
as a follow-up; the broadcast row stays hidden for kind-1 rumors.

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:54 +00:00
Claude 7985377a38 feat: private un-react via gift-wrapped deletions + force-private zaps on private notes
Gift-wrapped un-react:
- NIP17Factory.createDeletionNIP17 wraps a NIP-09 deletion to explicit
  recipients + self-copy, so the retracted rumor id never reaches public
  relays; DeletionIndex keys by (id, pubkey) and rumor pubkeys are forced
  to the seal's, so wrapped deletions are authenticated on receive
- Account.deletePrivately sends the wrapped deletion to the target
  rumor's participants (author + tagged users)
- AccountViewModel.reactToOrDelete now partitions reactions: public ones
  get a public NIP-09, rumor reactions get a wrapped one — un-react on
  private notes and NIP-17 chats works instead of no-op

Force-private zaps on private rumors:
- AccountViewModel.zap forces ZapType.PRIVATE for empty-sig targets
  (NONZAP kept: no receipt at all is even more private)
- ZapCustomDialog only offers Private/None for private targets
- Zap button re-enabled on private rumors; nutzap (public kind 9321) is
  refused with an explanatory error and the onchain rail is hidden, as
  both would e-tag the rumor id publicly
- Note: the LN provider's public 9735 receipt still carries the e-tag —
  the private zap type protects sender identity and comment, not the
  zapped id itself

Also verified: ReactionEvent consume counts empty-sig rumors (wasVerified
path) so wrapped reactions tally correctly.

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:53 +00:00
Claude 880995e17c feat: editable Notify block — pick receivers for private posts
Phase 3 of the private-notes plan: the composer's Notify row gains an
'+ Add' chip backed by the existing user-suggestion search, so users can
p-tag people who aren't cited in the text — for any post, public or
private. While the private toggle is ON the row is always visible,
relabeled 'Visible to' (the p-tags ARE the audience of the wrap), and an
empty list shows a 'only you will see this' hint for self-only notes.

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:52 +00:00
Claude 0fc81cf79d feat: compose private replies and private posts via NIP-17 gift wraps
Phase 2 of the private-notes plan: the short-note composer gains a
private (lock) toggle that gift-wraps the kind-1 to its p-tagged users
plus a self-copy instead of publishing it.

- NIP17Factory.createNoteNIP17: wraps a TextNoteEvent template to its
  taggedUserIds + the sender (only the unsigned rumor form travels)
- Account.sendPrivateNote: signs, wraps, and routes each wrap to the
  recipient's DM relays via the existing broadcastPrivately path
- ShortNotePostViewModel: wantsPrivateNote/privateNoteLocked state;
  forced ON and locked when replying to an unsealed rumor (and when
  reloading a drafted private reply); private wins over anonymous and
  scheduled modes so a locked reply can never fall through to a public
  publish path
- ShortNotePostScreen: lock toggle in the bottom action row; mutually
  exclusive with polls; schedule and anonymous hidden while private
- ReactionsRow: reply re-enabled on private rumors now that the
  composer locks privacy for them

Drafts stay enabled: TextNoteEvent does not implement ExposeInDraft, so
draft wrappers carry no anchor e-tags — the parent rumor id only exists
inside the NIP-44 encrypted draft content.

Verified by PrivateNoteFactoryTest: wraps cover p-tags + self, and the
recipient's unwrap yields a rumor with the same id and an empty sig
(the Note.isPrivateRumor() discriminator).

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:52 +00:00
Claude 7e7d8e5325 feat: private reactions on unsealed rumors + leak prevention for private notes
Unsealed NIP-59 rumors (private replies/posts arriving in gift wraps
from other clients) were indexed as ordinary notes: public reactions,
reposts, edits, pins, OTS timestamps, labels, public bookmarks, and
deletion requests could all e-tag the private rumor id onto public
relays.

- Note.isPrivateRumor(): empty-signature discriminator (rumors are the
  only notes materialized with an empty sig; draft inners are never
  indexed as standalone notes)
- ReactionAction: reactions inherit the target's privacy — empty-sig
  targets get gift-wrapped kind-7s fanned to the rumor author, every
  tagged user, and the sender's self-copy (add-only; un-react would
  need a public NIP-09 deletion that leaks the rumor id)
- AccountViewModel.reactToOrDelete: never NIP-09-delete rumor reactions
  (also fixes the same leak for existing NIP-17 chat reactions),
  tracked-broadcast mode excluded for rumor targets
- ReactionsRow: hide reply/boost/zap on private rumors (each publishes
  a public e-tag of the target); like stays, now wrapped
- DropDownMenu/NoteQuickActionMenu: hide broadcast, edit, timestamp,
  pin, hashtag label, public bookmarks, deletion request for rumors;
  private bookmarks and block/report stay available
- Lock badge in the note header (reuses existing Lock glyph, no font
  regen needed)

Covered by ReactionActionTest (public vs rumor fan-out, jvmTest green).
Plan: commons/plans/2026-06-10-private-replies-reactions-posts.md

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:51 +00:00
Vitor PamplonaandGitHub e8ed9379b8 Merge pull request #3181 from vitorpamplona/claude/focused-knuth-4zi90p
Fix ThumbnailDiskCache to recreate cache dir if cleared at runtime
2026-06-11 16:45:43 -04:00
Vitor PamplonaandGitHub 77a26ed131 Merge pull request #3180 from vitorpamplona/claude/dazzling-sagan-1c0ncz
Optimize string resource loading in WalletScreen
2026-06-11 16:40:07 -04:00
Claude 9ebee56dba fix: recreate thumbnail cache dir before writing temp file
The profile_thumbnails_v2 dir was only created in ThumbnailDiskCache's
constructor, but Android can delete cache subdirectories while the app
runs (system cache trim under storage pressure, or the user tapping
Clear cache in Settings). After that, every generateFromFile call
failed with ENOENT on the temp-file write until process restart,
silently disabling thumbnail caching.

Recreate the dir right before the write, and add instrumented
regression tests covering the cleared-at-runtime path.

https://claude.ai/code/session_01RQinCw5QKpaYXqtyb4gf3h
2026-06-11 20:16:37 +00:00
Claude 4563bf872d fix(profile): harden Send Payment screen after audit
Audit fixes for the unified payment screen:
- Re-peek cashu nutzap funding when the profile data refreshes so a
  late-arriving kind:10019 doesn't keep the Cashu rail hidden, and read
  the on-chain backend availability live instead of freezing it at first
  composition.
- Seed the active CLINK offer only while unset so a kind:0 refresh
  mid-flow can't discard an expired-or-moved redirect; drop the !!
  derefs in the offer range check.
- Marshal payment-callback stage updates to the Main scope (matching the
  app's progress-callback convention) and run the on-chain send off the
  Main thread since the sender signs the PSBT on the calling thread.
- Launch the external-wallet intent from the Main scope instead of the
  invoice fetcher's IO callback.
- Restore the old LN-address error affordance: payment failures now
  offer 'Message the recipient about this', opening a DM prefilled with
  the failure detail.
- Reuse the wallet sheet's FeeTier instead of a duplicated enum, memoize
  the zap-type options, and hoist the lightning target-type set.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 20:15:05 +00:00
Claude 6775ad8e62 docs: add RUNSTR interop research plan (kind 1301 + related events)
Catalogs every Nostr kind the RUNSTR app publishes/consumes, the exact
1301 tag dialect, the Supabase-migration caveats, and a phased plan for
Quartz event classes and Amethyst fitness screens.

https://claude.ai/code/session_01Kpx53UEeJqqR7CASzMu6GB
2026-06-11 20:12:30 +00:00
Claude 38023dac45 fix: resolve iOS test-name compile error and LocalContext lint error
- Rename Nip05Test backticked test name to drop parentheses, which are
  illegal identifier characters on Kotlin/Native (iosSimulatorArm64).
- Resolve CLINK budget toast strings at composition time via stringRes
  instead of context.getString inside the async callback, fixing the
  LocalContextGetResourceValueCall lint errors in WalletScreen.

https://claude.ai/code/session_01UgP8ErzBbQYkTDtkJx5nrt
2026-06-11 20:10:20 +00:00
Vitor PamplonaandGitHub 6c8dea755d Merge pull request #3178 from vitorpamplona/claude/kind-lamport-dwtzh8
Marmot: seed subscription since from stored messages on restart
2026-06-11 16:08:39 -04:00
Claude 73a63cf43a fix: address audit findings on the MLS unread/restore changes
- Clamp the seeded kind:445 subscription since at wall-clock now: the
  inner createdAt is sender-controlled, so a single future-dated message
  could push since past the present and silently skip genuinely new
  events on every restart. Covered by a new regression test.

- Drop the remember() around the group-list unread count: the chatroom's
  message set can shrink without newestMessage or lastReadTime changing
  (pruning, kind:5 deletion of an older message), which left the cached
  count stale. The set is pruned to ~100 entries, so counting per
  recomposition is cheap.

- Extract marmotGroupLastReadRoute(): the "MarmotGroup/<id>" last-read
  key was inlined at three call sites; a prefix drift between the
  mark-as-read side and the unread checks would silently reintroduce
  the bug this branch fixes.

- Derive GROUP_EVENT_REFETCH_OVERLAP_SEC from TimeUtils.ONE_DAY instead
  of re-deriving 24*60*60.
2026-06-11 20:05:25 +00:00
Claude 7d7f2f275f refactor(quartz): dedicated NutzapEvent.buildToUser for profile nutzaps
Restores the non-null zappedEvent on NutzapEvent.build and adds a
separate buildToUser builder (p tag only, no e/k tags) for nutzaps that
target a profile instead of an event — mirroring NIP-57's profile zap
convention. CashuWalletOps.sendNutzap dispatches between the two.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 19:48:56 +00:00
Claude f2702f9299 feat(profile): unified Send Payment screen for lightning, clink, on-chain and cashu zaps
Replaces the click-to-expand payment cards on the profile page with a
dedicated Send Payment screen that collects amount, optional message and
zap type, pays on the spot through the selected rail, and shows the
invoice-request + payment progress in the screen itself before closing.

- New Route.SendPayment(userHex, method, lnAddressOverride) with a
  stateless SendPaymentContent (previews for editing, fixed-price clink,
  in-progress, success and failure states).
- Rails offered per profile: Lightning (lud16/lud06 or a lightning
  payment target), CLINK offer (kind-0 / NIP-05, with expired-or-moved
  redirect), on-chain NIP-BC (fee tier selector), and NIP-61 cashu
  nutzaps gated on a shared funded mint.
- Lightning rail keeps the Public/Private/Anonymous zap types and adds
  the Non-Zap (plain payment) option; clink is a direct payment; cashu
  and on-chain receipts are inherent to their protocols and noted as such.
- Paying from this screen skips the extra in-app wallet confirmation
  dialog: the explicit amount + Pay tap is the confirmation.
- Profile LN-address row, CLINK chip, lightning payment-target chips and
  the wallet button's pay action now navigate to the new screen; other
  target types keep their external payto/URI behavior.
- NutzapEvent.build / CashuWalletState.sendNutzap now accept a null
  zapped event so nutzaps can target a profile (p-tag only), and
  AccountViewModel gains sendNutzapToUser + a zapType override on
  sendSats.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 19:28:02 +00:00
Claude 67ea7c5c95 Merge remote-tracking branch 'origin/main' into claude/sweet-shannon-smjotq 2026-06-11 19:01:36 +00:00
Claude 438f37a1ad Merge remote-tracking branch 'origin/main' into claude/kind-lamport-dwtzh8 2026-06-11 19:00:39 +00:00
Claude 0c9a7ed9ee Merge remote-tracking branch 'origin/main' into claude/friendly-lovelace-aw7vfj 2026-06-11 19:00:33 +00:00
Claude 9ce3b91f81 feat: route replies to private zaps into the sender's DM room
A public kind 1111 reply can never reach a private zapper: tagging the
decrypted sender would publicly expose them, so the composer correctly
refuses to — leaving the reply addressed to no one. Since only the zap
recipient can decrypt the sender, long-pressing a private-zap chip now
opens the DM room with that sender instead of the public comment
composer. Public and anonymous zaps keep the public reply path, and a
private zap we could not decrypt falls back to it as well.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-11 19:00:32 +00:00
Vitor PamplonaandGitHub 675580640d Merge pull request #3177 from vitorpamplona/claude/trusting-mayer-6o0yd5
Implement CLINK (Common Lightning Interface for Nostr Keys)
2026-06-11 14:42:25 -04:00
Claude d242eb62aa Merge remote-tracking branch 'origin/claude/trusting-mayer-6o0yd5' into claude/trusting-mayer-6o0yd5 2026-06-11 18:18:57 +00:00
Claude 990c5afe99 Merge remote-tracking branch 'origin/main' into claude/trusting-mayer-6o0yd5
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt
2026-06-11 18:15:04 +00:00
Vitor PamplonaandClaude Opus 4.8 44a6ab6bd4 fix(clink): keep offer/debit payments on clearnet + short subscription id
Two bugs kept CLINK offer/debit round-trips from completing over the shared
account relay client:

- The offer relay was treated as a generic "new" relay, so with Tor on it
  was dialed through the proxy and failed on services that block Tor exits.
  Register the offer/debit relays as money-operation relays for the duration
  of the round-trip; the subscribe()-triggered reconnect plus the
  BasicRelayClient wrong-transport rebuild then move the socket to clearnet.

- The subscription id "clink-offer-<event id>" was 76 chars; relays cap REQ
  subscription ids at 64 (NIP-01) and reject the over-long REQ outright, so
  the reply never arrived. Use newSubId(); the reply is matched by request
  id in the listener, not by subscription id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:00:32 -04:00
Vitor PamplonaandClaude Opus 4.8 f9f7de3ed0 feat(tor): money-operations relay category
Relay-socket Tor routing only had localhost/onion/DM/trusted/new buckets,
so a wallet or payment-service relay fell through to newRelaysViaTor and
got forced over Tor regardless of the "Money operations via Tor" toggle
(which previously governed only HTTP clients). On services that block Tor
exits this silently broke NIP-47 and CLINK payments.

Add a moneyOperationsViaTor field to TorRelaySettings and a moneyOpRelay
bucket to TorRelayEvaluation (taking precedence over DM/trusted/new, after
the onion reachability check). TorRelayState gains a persistent money-op
relay set — fed across all accounts from NIP-47 wallet relays and saved
CLINK debit relays via AccountsTorStateConnector — plus a reference-counted
ad-hoc registry for one-off payment relays (e.g. an noffer pointer). The
websocket builder resolves the per-relay decision from live source values
so ad-hoc registration takes effect on the next connect with no race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:35:11 -04:00
Vitor PamplonaandClaude Opus 4.8 d1bd5734cd fix(relay): rebuild sockets opened on the wrong transport
connectAndSyncFiltersIfDisconnected() bailed whenever a socket already
existed, so a still-connecting socket built for the wrong transport (e.g.
a relay whose Tor classification changed since the dial started) could
never be preempted — it blocked until the hung dial timed out. The
connected-relay path in RelayPool.reconnectIfNeedsTo already rebuilds
ready sockets via needsToReconnect(); this covers the connecting state it
cannot see (isConnectionStarted() true but isConnected() false).

Now: if a socket exists but reports needsReconnect() (transport/proxy
mismatch against the current builder decision), drop it and redial on the
correct transport; otherwise leave it. Disconnected relays still honor
their reconnect backoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:34:49 -04:00
Vitor PamplonaandGitHub 226ceee6df Merge pull request #3175 from nrobi144/feat/desktop-vlcj-to-kdroidfilter
feat(desktop): replace vlcj (GPLv3) with kdroidFilter ComposeMediaPlayer + JCodec/FFmpeg
2026-06-11 12:11:53 -04:00
nrobi144 eae1ed88ec fix(desktop,media): address PR #3175 review findings
Correctness fixes in GlobalMediaPlayer.kt
- snapshotFlow { hasMedia } collector for initial seek used `return@collect`
  which only exits the lambda; the collector kept running and each
  subsequent playVideo() call accumulated a live collector that would
  re-fire a stale seekTo() on the wrong media. Replaced with `Flow.first`
  which terminates the collection cleanly.
- playVideo()/playAudio() reset the public MediaPlaybackState to
  volume=100/isMuted=false on a new URL, but the kdroidFilter player
  retains its `volume` across openUri(); muting one track and starting a
  new one left the engine silent while the UI showed unmuted. Reset
  `player.volume = 1f` to match the public state.
- ensureVideoPlayer()/ensureAudioPlayer() called createVideoPlayerState()
  synchronously from the Compose getter; if native init throws (missing
  GStreamer on Linux, broken NativeLibraryLoader extraction) the whole
  window would crash. Wrapped in runCatching and changed
  activeVideoPlayerState to nullable. Consumers in DesktopVideoPlayer
  and GlobalFullscreenOverlay handle the null path by rendering the
  thumbnail / blank backdrop respectively; playVideo()/playAudio()
  surface "Video playback unavailable" through the existing
  errorReason -> PlaybackErrorMessage path.

Crash mitigation (kdroidFilter 0.10.0 UAF in MacVideoPlayerSurface)
- NowPlayingBar previously mounted a SECOND VideoPlayerSurface against
  the same VideoPlayerState while the feed card was already mounting
  one, doubling the draw rate against the shared frame bitmap and
  widening the UAF window in MacVideoPlayerSurface's RasterFromBitmap
  path. Mini-preview now renders the cached thumbnail (or the music
  icon fallback). 0.10.1 contains an upstream fix
  ("recover video playback after composition removal") but is not yet
  on Maven Central — single-surface mounting is the only mitigation
  we can ship today.

VideoThumbnailCache.kt
- Truncated-download cache poisoning: when an origin ignored the
  Range: header and returned HTTP 200 with the full body, we capped
  the copy at MAX_THUMB_BYTES and persisted the truncated file
  forever. Subsequent thumbnail attempts hit the broken cache file
  and re-failed JCodec/ffmpeg every time. Tag download results with
  whether the server actually returned 206; on 200, extract from the
  temp file and delete it (no persistent cache hit).
- Tor bypass: replaced the bare OkHttpClient with
  DesktopHttpClient.currentClient() so thumbnail fetches respect the
  user's Tor preference (fail-closed when Tor is expected but
  bootstrapping).
- ffmpeg version probe leaked the process on hang: now drains stdout
  to DISCARD and calls destroyForcibly() on timeout.
- Frame-extract ffmpeg subprocess could deadlock on a chatty stderr
  pipe: redirectError(DISCARD) so we never wait on stderr; a finally
  block destroys the process if anything leaked through the timeout.

CI workflow cleanup
- Removed vlc-setup download cache + pre-fetch steps from
  build.yml and smoke-test-desktop.yml. They were targeting an
  ir.mahozad.vlc-setup plugin we no longer apply, so they wasted
  ~minutes of CI time per leg and tied the build to videolan.org
  reachability for no reason.
- Trimmed create-release.yml's stale VLC-plugins justification on
  the linuxdeploy-vs-appimagetool comment.

.gitignore + missing per-OS ffmpeg READMEs
- The pre-PR rules blanket-ignored desktopApp/src/jvmMain/appResources/{linux,macos,windows}/
  so the LGPL FFmpeg drop-in slot READMEs created in 704f4f44e never
  reached the commit. Refined the ignore rules to keep stale vlc/ workspace
  trees out of git (still ignored) while explicitly tracking the
  ffmpeg/README.md drop-in slot under each OS. The READMEs document the
  recommended LGPL build source per OS for the bundled-FFmpeg packaging
  path.

Verified on macOS arm64:
  ./gradlew :desktopApp:compileKotlin   BUILD SUCCESSFUL
  ./gradlew :desktopApp:test            BUILD SUCCESSFUL
  ./gradlew :desktopApp:spotlessApply   clean

Refs PR #3175 review by @davotoula.
2026-06-11 17:46:12 +03:00
David KasparandGitHub 013b029c86 Merge pull request #3176 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-11 16:36:23 +02:00
Crowdin Bot aa63d01b32 New Crowdin translations by GitHub Action 2026-06-11 11:47:22 +00:00
David KasparandGitHub 80b9c91c59 Merge pull request #3174 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-11 13:45:08 +02:00
Róbert NagyandGitHub e3441157ad Merge branch 'main' into feat/desktop-vlcj-to-kdroidfilter 2026-06-11 13:50:42 +03:00
nrobi144 704f4f44ee feat(desktop): replace vlcj with kdroidFilter ComposeMediaPlayer + JCodec/FFmpeg
Drops uk.co.caprica:vlcj 4.8.3 (GPL-3.0) from desktopApp and replaces it
with an MIT-dominant stack:

- Video / audio playback: io.github.kdroidfilter:composemediaplayer:0.10.0
  (MIT) — OS-native backends (Media Foundation on Windows, AVFoundation on
  macOS, GStreamer on Linux). First-class Compose VideoPlayerSurface.
- Thumbnail extraction: org.jcodec:jcodec(+javase):0.2.5 (BSD-2) primary
  H.264 path, raw ProcessBuilder FFmpeg fallback for HEVC / VP9 / AV1 /
  HLS / non-faststart MP4.
- Binary SPDX: MIT AND LGPL-2.1-or-later AND BSD-2-Clause AND Apache-2.0.
  rpmLicenseType updated accordingly (previously misdeclared as MIT
  while shipping GPLv3 vlcj).

Code changes:
- Deleted: VlcjPlayerPool, MacOsVlcDiscoverer, BundledVlcDiscoverer,
  VlcResourceResolver
- Rewrote: GlobalMediaPlayer (kdroidFilter engine + snapshotFlow-based
  state sync into the preserved MediaPlaybackState contract);
  VideoThumbnailCache (JCodec → ProcessBuilder ffmpeg cascade, with a
  hard 4 MiB download cap, Content-Type sniff to reject HTML error
  pages, and cleanup of zero-byte cache entries); DesktopVideoPlayer
  (mounts VideoPlayerSurface for the active URL, codec/network error
  UX with "Open in default player" fallback)
- Updated: NowPlayingBar + GlobalFullscreenOverlay to render
  VideoPlayerSurface directly (drops the videoFrame ImageBitmap relay)
- Main.kt: drops vlcj pre-init / shutdown calls (kdroidFilter lazy-loads
  natives + registers its own shutdown hook on Windows)

Build / packaging:
- Removes ir.mahozad.vlc-setup plugin + vlcSetup{} block + the per-OS
  bundled VLC tree + the -Dvlc.plugin.path JVM arg
- Adds NOTICE.md + per-component LICENSE-*.txt under appResources/common
  (LGPL-2.1 license text is a placeholder — replace with verbatim FSF
  text before release)
- Adds per-OS LGPL FFmpeg drop-in directories with README pointing at
  the recommended LGPL binary source (osxexperts.net / Crigges Windows
  LGPL build)
- Adds Flathub manifest skeleton (Gitnuro-style: org.freedesktop.Platform
  24.08 + openjdk21 extension + org.freedesktop.Platform.ffmpeg-full
  add-extension for patent codecs)
- AppRun: drops VLC LD_LIBRARY_PATH / VLC_PLUGIN_PATH env wiring

Verified on macOS arm64:
- ./gradlew :desktopApp:compileKotlin                BUILD SUCCESSFUL
- ./gradlew :desktopApp:test                         BUILD SUCCESSFUL
- ./gradlew :desktopApp:spotlessApply                clean
- Smoke launch: no VLC/vlcj/libvlc log lines, kdroidFilter native
  library extracts to ~/.cache/composemediaplayer/native/, thumbnail
  cache populates at ~/.cache/amethyst-desktop/video-thumbs/ with the
  4 MiB cap enforced
- H.264 MP4 playback (active + thumbnail extraction) confirmed
- VP9-in-WebM playback fails on macOS as AVFoundation cannot decode it —
  expected codec gap; surfaced via PlaybackErrorMessage + "Open in
  default player" handoff in DesktopVideoPlayer

Docs:
- docs/plans/2026-06-11-feat-replace-vlcj-with-kdroidfilter-plan.md
- docs/plans/2026-06-11-vlcj-replacement-testing-sheet.md
2026-06-11 13:37:33 +03:00
Claude 049db06844 feat: pin DM conversations to the top of the chat list
Long-pressing a private chat row in the messages tab now offers Pin to
top / Unpin. Pinned rooms sort above everything else in the known-chats
list (ties broken by the usual newest-first order) and show a small pin
icon next to the room name.

Pins are stored per account as a local-only setting (encrypted
SharedPreferences via AccountSettings.pinnedChatrooms) because there is
no standard NIP-51 list for pinned DMs; this can be migrated to a synced
list later if one is standardized.

https://claude.ai/code/session_0131YwG6bE3yH8Kk9MxjMA5i
2026-06-11 01:36:16 +00:00
Claude 7050264d70 fix: clear cachedAccounts entry when an account is deleted
LocalPreferences.deleteAccount() wiped the encrypted preference file but
left the in-memory cachedAccounts entry behind, so deleting and re-adding
the same account could resurrect stale settings from the cache.

https://claude.ai/code/session_0131YwG6bE3yH8Kk9MxjMA5i
2026-06-11 01:36:03 +00:00
Claude 0bfd2f8bc2 test: move MarmotManagerLeaveRejoinTest to jvmTest so CI actually runs it
The test lived in commons androidHostTest, but no CI workflow or
pre-push task runs :commons:testAndroidHostTest — and running it
manually fails before reaching any assertion: quartz's android
PlatformLog actual hits unmocked android.util.Log stubs
(NoSuchMethodError), since the source set is not configured with
returnDefaultValues. The end-to-end leave/rejoin coverage was
therefore never executed anywhere.

:commons:jvmTest runs in CI and in the pre-push hook, already has the
secp256k1 JVM bindings the test needs, and uses quartz's JVM logger.
Verified green there alongside MarmotManagerRestoreTest.
2026-06-11 01:20:59 +00:00
Claude c6b09c4b53 fix: anonymous profile zaps were encrypted as private zaps; nutzap chips reply on long-press
The user-only LnZapRequestEvent.create overload marked ANONYMOUS requests
with a blank-valued anon tag, which the signer treats as an unsigned
private zap: the message was encrypted to the recipient under the
throwaway key instead of staying public. Use the valueless anon tag, as
the event-targeted overload already does. Adds a regression test.

Also carries the nutzap note into the notification gallery chips so the
long-press reply-to-zap gesture works for NIP-61 nutzaps too — no extra
tagging needed there since nutzaps are signed by the sender.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-10 23:41:26 +00:00
Crowdin Bot ea74be65cf New Crowdin translations by GitHub Action 2026-06-10 23:30:11 +00:00
Vitor PamplonaandGitHub 2e26823f3d Merge pull request #3166 from davotoula/fix/nip71-legacy-video-addressable-dtag
Compute legacy NIP-71 video addresses with their d tag
2026-06-10 19:28:27 -04:00
Vitor PamplonaandGitHub 765572b25b Merge pull request #3173 from vitorpamplona/claude/lucid-bardeen-tiedwa
Audit & refresh skill library, docs, and hooks (Phase 2–3)
2026-06-10 19:28:18 -04:00
Claude c503af0f5a docs: fix stale signer, NIP-19, NIP-44, and EventStore claims in skills
Second audit pass over the remaining skills (amy-expert, auth-signers,
find-*, nostr-expert, quartz-integration, vendored technique skills),
verifying every concrete claim against the code:

- auth-signers: bunker login goes through NostrSignerRemote.fromBunkerUri
  + connect(), not the nonexistent RemoteSignerManager.connect(url)
- nostr-expert: NIP count 57 -> 80+; replace invented Nip44v2/Nip19
  static APIs with the real Nip44 facade, ByteArray bech32 extensions,
  entity create() helpers, and Nip19Parser.uriToRoute()?.entity
- nip-catalog: heading counts corrected to 87 standard + 23 experimental
  packages with a ground-truth pointer
- quartz-integration: NIP-19 example rewritten for ParseReturn.entity;
  Event Store is commonMain (all platforms), not Android-only, with the
  real store.sqlite.EventStore import and suspend query<T> API

amy-expert, find-missing-translations, find-non-lambda-logs, the rest of
auth-signers, and the vendored technique skills audited clean.

https://claude.ai/code/session_01EC7LdXjatFTh1CJSP4qKRn
2026-06-10 23:21:30 +00:00
Claude 2eb6510eec fix: stop refetching the full MLS kind:445 backlog on every restart
The Marmot subscription since, the processed-event dedup set, and the
application ratchet position (group state persists only at commits) are
all in-memory only. On restart, relays therefore redeliver the group's
entire kind:445 history and the rewound ratchet re-decrypts old
application messages as if they had just arrived — wasted decryption
work and, when a replay beats the disk restore, duplicate entries
appended to the persisted plaintext message log.

Two defenses:

- MarmotManager.restoreAll() now seeds each restored group's
  subscription since from the newest persisted decrypted message, minus
  a one-day overlap window for late/out-of-order publishes. Seeding
  happens before syncWithGroupManager registers default entries, so
  even the first filter set sent to relays carries it. The CLI is
  unaffected: it builds group filters from its own persisted since.

- MarmotMessageStore appends are now explicitly idempotent (contract
  was previously ambiguous and both real stores appended blindly):
  the Android and CLI file stores skip an entry that is already in the
  group's log, so replays inside the overlap window cannot grow it.

Covered by MarmotManagerRestoreTest in commons jvmTest — placed there
rather than androidHostTest because CI only runs :commons:jvmTest (the
androidHostTest task currently fails on android.util.Log stubs even
for the pre-existing Marmot test).
2026-06-10 23:03:48 +00:00
Claude e4ce7b887d feat: support replying to zaps from the notification screen
Zap receipts (kind 9735) are signed by the recipient's lightning provider,
not by the person who zapped, so both the reply tagging and the rendering
around replies-to-zaps need the sender resolved from the embedded kind 9734
zap request:

- Long-press on a zap chip in the notification galleries (MultiSetCard and
  ZapUserSetCard) opens the NIP-22 comment composer targeting the zap
  receipt, reusing the existing generic-comment fallback in routeReplyTo.
- CommentPostViewModel now p-tags the zap request author when replying to a
  zap so the zapper actually gets notified (the receipt's own author tags
  point at the custodian). Requests carrying an anon tag (anonymous or
  private zaps) are skipped: the embedded key is ephemeral and tagging the
  decrypted sender of a private zap would publicly expose them.
- The notifying chip row shows the zapper and removing the chip is
  respected, including across draft reload.
- FirstUserInfoRow and the compact reply-to label now display the zap
  sender (decrypted for private zaps, locally only) instead of the wallet
  service when the note or the replied-to parent is a zap receipt.
- ZapAmountCommentNotification carries the receipt note so chips can act on
  the zap itself.
- Adds LnZapRequestEvent.hasAnonTag() with tests covering public, anonymous
  and private zap requests.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-10 22:55:25 +00:00
Claude af7d61f361 fix: persist MLS chat unread state across app restarts
The unread dot for Marmot/MLS group rooms was driven by an in-memory
unreadCount on MarmotGroupChatroom. On restart the MLS group state is
restored from the last persisted commit, the kind:445 subscription
restarts with since=null, and the in-memory processed-event dedup set is
empty — so relays redeliver old group events, they re-decrypt as fresh
application messages, and the counter was re-bumped, resurrecting the
dot for chats already read.

Marking-as-read was already persisted: opening a group chat writes the
newest rendered message's createdAt to the MarmotGroup/<groupId> route
in lastReadPerRoute (saved to disk with account settings). Compute the
unread indicators from that timestamp instead — exactly how DM rooms
and public channels do it — in both the Messages screen row and the
Marmot group list row.

With no consumer left, drop the volatile counter and collapse the
addMessageSync/restoreMessageSync split (they only differed in the
counter bump).
2026-06-10 22:08:51 +00:00
Claude 8209f4416a docs: fix stale claims in Claude skills against current code
Audit pass that verified every concrete claim in .claude/ against the
repository:

- account-state: Account.kt no longer exposes followListFlow-style
  StateFlows; document the state-object pattern (kind3FollowList,
  muteList, bookmarkState, ... each exposing .flow) and rewrite the
  catalog reference from the real Account.kt
- feed-patterns: filter bases (FeedFilter, AdditiveFeedFilter,
  ChangesFlowFilter, FeedContentState) moved to commons/ui/feeds;
  ui/dal keeps AdditiveComplexFeedFilter/FilterByListParams plus
  back-compat typealiases; fix recipe example signatures
- relay-client: add nip17Dm/, eoseManagers and subscriptions entries
  to the layout tree
- gradle-expert: 4-module claim -> 10 modules; refresh compose/kotlin/
  BOM versions; rewrite dependency graph with verified edges for cli,
  geode, quic, nestsClient, quic-interop, benchmark
- desktop-expert: drop drifted Main.kt line numbers; sidebar is the
  custom MainSidebar in DeckSidebar.kt, not a NavigationRail in
  SinglePaneLayout.kt
- android-expert: compileSdk/targetSdk 36 -> 37, versionName via
  generateVersionName()
- kotlin-expert: remove reference to nonexistent commit 258c4e011
- CLAUDE.md: add missing geode/benchmark/quic-interop modules
- desktop-run: packageRpm + correct binaries output path; extract.md:
  drop duplicated find clause
- session-start.sh: /home/user/Amber fallback was a copy-paste from
  another repo; fall back to CLAUDE_PROJECT_DIR

https://claude.ai/code/session_01EC7LdXjatFTh1CJSP4qKRn
2026-06-10 22:01:29 +00:00
Claude f2cce3dc87 fix(clink): run offer/debit payer crypto off the Main thread
StrictMode flagged the offer round-trip (ephemeral keygen, JSON serialization,
NIP-44 encrypt/decrypt, signing) running on the UI thread, because
ClinkOfferPreview launches it from a Compose (Main) scope. Wrap the heavy work
in withContext(Dispatchers.IO) in both ClinkOfferPayer.requestInvoice and
ClinkDebitPayer.payInvoice/requestBudget so the payers are main-safe regardless
of caller dispatcher.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 21:40:53 +00:00
Claude 5aab19e8da feat(clink): copy-offer button on the offer card title
Adds a ContentCopy IconButton at the right of the CLINK Offer card title that
copies the noffer string (the active pointer, after any moved-offer redirect) to
the clipboard with a confirmation toast.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 21:36:27 +00:00