Commit Graph
14071 Commits
Author SHA1 Message Date
Claude 44aa262363 fix(commons): tighter Base64Image contract + pin serializer wire format
Two follow-up cleanups from the audit.

Base64Image.parse: when the regex matched but the data capture group
was missing, the migrated version returned an empty ByteArray. The
original threw NPE (java.util.Base64.getDecoder().decode(null)). Both
behaviors are accidents — restore the intended contract: throw the
existing "Unable to convert base64 to image" Exception explicitly.

FeedDefinitionSerializerTest gains a serializesToExpectedWireFormat
test that pins the byte-exact JSON output for a representative
multi-field feed. The legacy-Jackson migration claimed byte-identity
but only round-trip and reverse-compat were covered. Any future change
to field ordering / null handling / number formatting now fails this
test loudly, protecting users who have saved feeds on disk and any
downstream consumer expecting the stable order.
2026-05-25 20:05:00 +00:00
Claude d1749c314f fix: audit findings on iOS-readiness migration
Address bugs and gaps surfaced by an audit of the prior 14 commits.
JVM tests passed because of typealias / platform-type lenience that
won't hold on Native; these are real iOS compile / behavior issues.

BUG fixes (iOS compile failures):

- commons/.../Note.kt:899 — Iterable.sumOf { -> BigDecimal } is a
  JVM-stdlib-only overload. Common stdlib ships sumOf only for
  Int/Long/Double/Float/UInt/ULong. Replaced with fold(BigDecimal(0)).
- commons/.../Note.kt:889 — BigDecimal(it.event?.content): the quartz
  expect-class constructor takes String non-null; JVM accepted nullable
  via platform-type lenience and threw NPE caught downstream. Switched
  to ?.let { content -> BigDecimal(content) }.
- commons/.../Note.kt:838 — `catch (e: java.lang.Exception)` -> `Exception`.
- commons/.../feeds/custom/FeedDefinitionBuilder.kt + FeedBuilderState.kt:
  inline FQN `java.util.UUID.randomUUID().toString()` -> kotlin.uuid.Uuid.
  random().toString() (Kotlin 2.0+, @OptIn ExperimentalUuidApi).
  inline `System.currentTimeMillis() / 1000` -> TimeUtils.now() (already
  used elsewhere in the codebase).
- commons/.../viewmodels/NestViewModelTest.kt: moved from commonTest to
  jvmTest. The test imports NestViewModel + nestsclient, both of which
  the prior PR moved to jvmAndroid. commonTest depends on commonMain
  only, so the test would fail to compile for iosSimulatorArm64Test.

SUBTLE fixes:

- commons/.../UserRelaysCache.kt: the flow field used double-checked
  locking on a non-volatile var. JMM hazard on Native (ARM weak memory
  model) — outer fast-path could observe a partially-published
  WeakReference. Added @kotlin.concurrent.Volatile.
- commons/.../util/UrlValidation.ios.kt: NSURL.URLWithString("http:")
  returns non-null with scheme="http" and no host; JVM's URI.toURL()
  rejects with MalformedURLException. Reject scheme-only network URLs
  (http/https/ws/wss/ftp without a host) to match JVM behavior.
- commons/.../util/KmpLock.kt commonMain doc: corrected "NSLock" ->
  "NSRecursiveLock" to match the actual iOS implementation.

verifyKmpPurity gate extended (commons + quartz):

- Adds patterns: System.currentTimeMillis, Thread.sleep, java.util.UUID,
  kotlin.jvm.Synchronized, kotlin.jvm.Volatile.
- Each pattern paired with a hint pointing at the canonical KMP
  replacement; the error message surfaces both.
- Skips lines that start with //, *, or /* to avoid false positives on
  KDoc / migration notes.
2026-05-25 02:17:32 +00:00
Claude f1845d6a06 feat(commons): add iOS actuals for KmpLock, WeakReference, isValidUrl
Pre-stages the three iosMain actuals that the macOS CI run is most
likely to demand once it compiles :commons for Native (the dev
container can't extract the K/N LLVM toolchain to validate locally).

- KmpLock.ios.kt: NSRecursiveLock — mirrors the ReentrantLock
  semantics the jvmAndroid actual exposes (reentrant per-thread).
- WeakReference.ios.kt: actual typealias to kotlin.native.ref.
  WeakReference<T> — same constructor + get(): T? shape as the
  jvmAndroid typealias to java.lang.ref.WeakReference<T>.
- UrlValidation.ios.kt: NSURL.URLWithString with an explicit scheme
  check, since NSURL is more permissive than JVM's URI.toURL() and
  accepts scheme-less relatives that the JVM contract rejects.

Lands together so the next CI run's failure mode (if any) is more
informative than "iosArm64 unresolved reference" three times over.
2026-05-25 00:44:38 +00:00
Claude e02972e1be build(commons): enable iosArm64 + iosSimulatorArm64 targets
Phase 2 task 10 of the iOS plan — flip on iOS targets for :commons.
Gradle dep resolution is fully green for iOS; actual Kotlin/Native
compilation runs on the macOS CI job (the dev container in which this
was authored can't extract the K/N LLVM toolchain).

Dep reshuffle to match what's actually KMP-available:

- commonMain: kept project(":quartz"), Compose Multiplatform, coil-compose,
  androidx-collection, kotlinx-collections-immutable, kotlinx-serialization-json,
  compose components-resources, androidx-lifecycle-viewmodel,
  androidx-lifecycle-runtime-compose. These all publish iosArm64 +
  iosSimulatorArm64 variants per `.module` inspection.

- jvmAndroid (NEW location): project(":nestsClient") (JVM+Android-only),
  coil-okhttp (JVM-only), markdown-commonmark / markdown-ui /
  markdown-ui-material3 (the RenderMarkdown.kt consumer is already in
  jvmAndroid), and androidx-lifecycle-viewmodel-compose (AndroidX publishes
  android + jvmStubs + linuxx64Stubs variants — no iOS, so the viewModel()
  Composable helper stays JVM-bound until we either swap to the
  org.jetbrains.androidx.lifecycle variant or accept a platform-specific
  ViewModel access pattern on iOS).

- libs.versions.toml: adds androidx-lifecycle-viewmodel catalog entry.

- New intermediate source set iosMain → both iosArm64Main and
  iosSimulatorArm64Main depend on it (clean place for iOS-only actuals
  when KmpLock, WeakReference, etc. get their iOS implementations).

- .github/workflows/build.yml: test-quartz-ios job now also runs
  :commons:compileKotlinIosArm64 + :commons:compileKotlinIosSimulatorArm64.
2026-05-25 00:13:13 +00:00
Claude 880c1bfd4a refactor: clear final java.* imports from commons/commonMain
Three changes that bring commons/commonMain to zero java.* imports
(down from 18 at the start of Phase 2).

- EventListMatchingFilter, NoteListMatchingFilter: moved to jvmAndroid.
  Both use ConcurrentSkipListSet + SortedSet for ordered concurrent
  iteration, and their only consumer is LocalCache in the Android app.
  iOS-time we can revisit if a KMP ordered concurrent set is needed.

- Note.kt's BigDecimal: switch import from java.math.BigDecimal to
  quartz's existing expect/actual com.vitorpamplona.quartz.utils.BigDecimal.
  BigDecimal.ZERO -> BigDecimal(0); BigDecimal.valueOf(longVal) ->
  BigDecimal(longVal) (the expect class already has the Long
  constructor). NoteOnchainZapTest gets the same treatment.

- Adds two top-level extensions in quartz commonMain (separate
  BigDecimalOps.kt file to avoid the duplicate-JVM-classname collision
  with the existing BigDecimal.kt actuals):
    operator fun BigDecimal.plus(other: BigDecimal)
    operator fun BigDecimal.minus(other: BigDecimal)
  Lets += / + / - continue to work on commonMain BigDecimal values.

Commons/commonMain is now structurally iOS-ready as far as the
java.* import audit can tell. Remaining iOS work: actually flip on
the iOS targets, see what UI / dep transitives break, and address.
2026-05-24 23:48:29 +00:00
Claude 90fbe06f19 refactor: KMP URL validation + move UrlInfoItem to jvmAndroid
Clears the two java.net.* importers from commons/commonMain.

- Adds expect fun isValidUrl(url: String?): Boolean in
  commons/commonMain/util/. The jvmAndroid actual preserves the
  exact JVM semantics (URI.toURL() + the same 3 catch arms);
  iOS actual will use NSURL when the target lands.
- RichTextParser.isValidURL becomes a thin wrapper around
  isValidUrl. Keeps the existing static call site so callers in
  the Android app and Desktop need no change.
- UrlInfoItem.kt (link-preview model that wraps URI) moves to
  jvmAndroid; its only consumers are the Android link-preview
  pipeline (HtmlCharsetParser, UrlPreviewState, UrlPreviewCard),
  which already live outside commonMain.

commons/commonMain is now down to 3 java.* importers: Note
(BigDecimal) and the two SortedSet-based observables.
2026-05-24 23:38:58 +00:00
Claude 39008f90d3 refactor: move feature-not-yet-iOS-ready files to jvmAndroid
Clears the remaining easy iOS blockers in commons/commonMain by
relocating files whose underlying feature isn't iOS-ready yet, rather
than fabricating expect/actuals we won't need until that feature ships.

- NestViewModel + ActiveSubscription: depend on :nestsClient (audio
  rooms — Phase 5 per the iOS plan). Moved as-is; both already lived
  in a jvmAndroid-shaped package.
- HtmlCharsetParser: depends on java.nio.charset.Charset, used only
  by the Android link-preview pipeline (no Desktop / iOS consumer
  today).
- RenderMarkdown: depends on com.halilibo.richtext.* — needs iOS
  artifact verification before it can return to commonMain (tracked
  for Phase 3).
- MediaContentModels.kt is split:
  * URL-based models (BaseMediaContent, MediaUrlImage/Video/Pdf,
    EncryptedMediaUrlImage/Video) stay in commonMain — pure KMP, no
    java.io.File reference.
  * Locally-cached variants (MediaPreloadedContent, MediaLocalImage,
    MediaLocalVideo) move to a new MediaLocalContent.kt under
    jvmAndroid — they hold a java.io.File and call .exists().

After this PR commons/commonMain has 5 remaining java.* importers
(Note's BigDecimal, the two SortedSet observables, URL parsing in
RichTextParser + UrlInfoItem). Down from 18 at the start of Phase 2.
2026-05-24 23:36:10 +00:00
Claude 6f1292bfcf refactor: replace @Synchronized / @Volatile with KMP primitives
Clears the last of the JVM-only synchronization annotations from
commons/commonMain so the model layer can compile on iOS. 15
methods across 4 files migrated.

- @Synchronized -> KmpLock.withLock { } with one per-instance syncLock
  field per class. Original semantics preserved: @Synchronized on
  methods of the same class synchronized on `this`, and a single
  per-instance KmpLock gives the same exclusion.

  * Channel.kt: addRelaySync, createOrDestroyFlowSync
  * Chatroom.kt: addMessageSync, removeMessageSync
  * MarmotGroupChatroom.kt: placeholderNote, addMessageSync,
    restoreMessageSync, removeMessageSync, clearAllMessagesSync
  * Note.kt: innerAddZap, innerAddOnchainZap,
    innerRemoveOnchainZapForSource, innerAddZapPayment, addRelaySync,
    createOrDestroyFlowSync

- Note.kt's @Volatile fields: now use kotlin.concurrent.Volatile
  (KMP) instead of kotlin.jvm.Volatile (JVM-only) via explicit
  import. Volatile semantics preserved on every target.

NestViewModel.kt also uses @Volatile (and the nestsClient project
dep); that file moves to jvmAndroid in a separate PR as planned
(audio rooms is Phase 5).
2026-05-24 23:06:30 +00:00
Claude 5c2f93f82f refactor: replace stately with LargeCache + KmpLock
Address review feedback: the project already has LargeCache (in quartz,
with jvmAndroid/appleMain/linuxMain actuals) as its KMP concurrent-map
abstraction — it's used pervasively in the model layer. Adding stately
duplicated that capability with an external dep.

- Comparable-key maps switch to LargeCache:
  * ChessEventCollector.moves (String key)
  * ChessEventCollectorManager.collectors (String key)
  * ChessRelayFetchHelper.events (String key)
  * ChessRelayFetchHelper.relayEventCounts: LargeCache<NormalizedRelayUrl,
    AtomicInt> with getOrCreate { AtomicInt(0) }.addAndFetch(1) — replaces
    the stately .block { compute } increment idiom. getOrCreate is atomic
    via ConcurrentSkipListMap.putIfAbsent so all threads end up
    incrementing the same AtomicInt instance.
  * ChessLobbyLogic.recentlyLoadedGames (String key)

- The SubscriptionManager pair (MutableComposeSubscriptionManager,
  ComposeSubscriptionManager) keeps a plain mutableMapOf — T :
  MutableQueryState is generic and not Comparable, so LargeCache's
  ConcurrentSkipListMap backing would ClassCastException at put time.
  Concurrency comes from a KmpLock-guarded map.

- Set-shaped uses switch to KmpLock + mutableSetOf:
  * ChessEventCollector.processedEventIds
  * ChessRelayFetchHelper.eoseReceived
  * ChessLobbyLogic.dismissedGameIds + seenEventIds (the bounded LRU
    keeps insertion-order eviction; mutableSetOf returns LinkedHashSet
    on every KMP target).

- UserRelaysCache.flow's lock: stately Lock -> KmpLock.

Adds expect class KmpLock() with jvmAndroid actual that wraps
ReentrantLock. iOS actual (NSLock) will land with the iOS target.
Mirrors the WeakReference pattern from the previous PR.

Drops stately-concurrent-collections 2.1.0 from libs.versions.toml and
commons/build.gradle.kts (no remaining consumers).
2026-05-24 21:10:35 +00:00
Claude 95beed16e1 refactor: KMP WeakReference + drop synchronized(this) from commonMain
Phase 2 of the iOS plan — clears the java.lang.ref.WeakReference
blocker from commons/commonMain. Four model files migrated; one
additional sync primitive replaced.

- Adds expect class WeakReference<T : Any> in
  commons/commonMain/util/, with a jvmAndroid actual that typealiases
  to java.lang.ref.WeakReference. iOS actual will typealias to
  kotlin.native.ref.WeakReference when the target is added.
- Channel / Chatroom / MarmotGroupChatroom: the WeakReference(null)
  initializer relied on platform-type nullability of
  java.lang.ref.WeakReference's constructor. With T : Any in the expect
  class, fields become nullable (WeakReference<...>? = null) and the
  .get() callsites become ?.get(). Behaviorally equivalent.
- UserRelaysCache: same WeakReference migration, plus the
  synchronized(this) double-checked-locking idiom is replaced with
  co.touchlab.stately.concurrency.Lock + withLock (KMP).
  kotlin.synchronized is JVM-only; Lock comes in transitively via
  stately-concurrent-collections already added in the previous PR.

Model-layer @Synchronized usage in Channel/Chatroom/MarmotGroupChatroom/
Note (also JVM-only) is a separate iOS blocker and a separate PR.
2026-05-24 18:30:27 +00:00
Claude bf6467cdcf refactor: drop ConcurrentHashMap from commonMain
Phase 2 of the iOS plan — clears the ConcurrentHashMap blockers from
commons/commonMain. Five files migrated (the four flagged in the
initial audit + ChessLobbyLogic, which used fully-qualified inline
java.util references that the import-based audit missed).

Adds co.touchlab:stately-concurrent-collections 2.1.0 — a small,
mature KMP library that provides ConcurrentMutableMap /
ConcurrentMutableSet with semantics equivalent to ConcurrentHashMap /
ConcurrentHashMap.newKeySet on every Kotlin target. The .block { }
helper covers the compound-update paths (ChessRelayFetchHelper's
per-relay event-count compute, ChessLobbyLogic's bounded-LRU dedup).

- ComposeSubscriptionManager + MutableComposeSubscriptionManager:
  ConcurrentHashMap -> ConcurrentMutableMap
- ChessEventCollector + ChessEventCollectorManager: map and Set
- ChessRelayFetchHelper: in-function event/relay state
- ChessLobbyLogic: replaces dismissedGameIds (synchronizedSet),
  recentlyLoadedGames (ConcurrentHashMap), seenEventIds (bounded LRU
  using LinkedHashSet via Collections.synchronizedSet + synchronized {}).
  seenEventIds keeps insertion-order eviction semantics because
  mutableSetOf returns LinkedHashSet on every KMP target.
2026-05-24 18:24:22 +00:00
Claude 1b6b699d76 refactor: drop java.util.concurrent atomics from commonMain
Phase 2 of the iOS plan — two of the ~9 small migrations to clear
java.* imports out of commons/commonMain.

- ChessLobbyState: the AtomicLong stateVersionCounter only existed to
  bump a MutableStateFlow<Long>. MutableStateFlow.update is itself
  atomic, so the counter is redundant — replaced with
  _stateVersion.update { it + 1 }. Removes the dep and simplifies the
  code.
- SigningState (GlobalSigningStatus): AtomicInteger is doing real
  cross-thread coordination. Migrated to kotlin.concurrent.atomics.
  AtomicInt (KMP stdlib). The common-API method names differ from
  AtomicInteger — addAndFetch(±1) / store(0) instead of
  incrementAndGet / decrementAndGet / set.
2026-05-24 18:11:20 +00:00
Claude 78ef4fa672 refactor: migrate Base64Image off java.util.Base64
Phase 2 of the iOS plan — first of ~9 small migrations to clear the
java.* imports out of commons/commonMain. Replaces java.util.Base64
with kotlin.io.encoding.Base64 (stdlib, KMP-clean). The two callers
(Android Base64Fetcher, Desktop DesktopBase64Fetcher) use the public
parse() signature only, which is unchanged.

Also documents the full Phase 2 audit in
amethyst/plans/2026-05-24-ios-support.md: out of 335 commonMain files,
21 are real iOS blockers grouped into ~10 small mergeable PRs. The
remaining 183 androidx.compose users and 7 androidx.lifecycle users
already map to JetBrains Compose Multiplatform / AndroidX KMP and need
no work.
2026-05-24 18:07:25 +00:00
Claude b27fc34786 refactor: migrate FeedDefinitionSerializer to kotlinx.serialization
The one Jackson holdout in commons/commonMain. Migrating it unblocks
the iOS purity gate for :commons (Phase 1 of the iOS plan).

- Rewrites FeedDefinitionSerializer with the kotlinx.serialization JSON
  tree API (JsonObject / JsonArray / JsonPrimitive). Wire format is
  byte-identical, so users' existing on-disk custom-feed definitions
  keep deserializing — covered by a new parsesLegacyJacksonOutput test
  that pins a hand-written Jackson-shaped JSON blob.
- Adds :commons:verifyKmpPurity (mirrors the one in :quartz) and wires
  it into the CI lint job alongside :quartz:verifyKmpPurity.
- Pulls in kotlinx-serialization-json as a commonMain dep; the
  serialization plugin was already applied on :commons.
2026-05-24 16:41:00 +00:00
Claude e0c3b18731 ci: add iOS test job for quartz + commonMain purity gate
Phase 1 of the iOS support plan (amethyst/plans/2026-05-24-ios-support.md).
Two independent guards so JVM-only imports can't silently appear in
quartz's iOS-bound source sets:

- :quartz:verifyKmpPurity (Linux, ~1s): scans commonMain + apple/native
  source sets for com.fasterxml.jackson / okhttp3 references and fails
  the build with a clear pointer to the offending file:line. Wired into
  the existing lint job so it runs on every PR.

- test-quartz-ios (macos-latest): runs :quartz:iosSimulatorArm64Test on
  the simulator (NIP-04, NIP-17, NIP-19, NIP-49 vectors + AES-GCM and
  chatroom-key tests already in quartz/src/iosTest) and additionally
  compileTestKotlinIosArm64 to catch device-variant compile drift.
2026-05-24 16:12:34 +00:00
Claude f10973c06e docs: add iOS support plan 2026-05-24 15:59:59 +00:00
Vitor PamplonaandGitHub 74a646c7eb Merge pull request #3040 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-23 17:30:01 -04:00
Crowdin Bot 05eb35a4ca New Crowdin translations by GitHub Action 2026-05-23 21:14:31 +00:00
Vitor PamplonaandGitHub 4758562f88 Merge pull request #3043 from vitorpamplona/claude/multi-npub-external-signer-login-U5GIb
Fix account cache race condition in setDefaultAccount
2026-05-23 17:12:55 -04:00
Vitor PamplonaandGitHub d82143e392 Merge pull request #3042 from vitorpamplona/claude/reaction-row-padding-bug-DaQO0
Fix reaction row layout for icon-only rightmost items
2026-05-23 17:09:55 -04:00
Claude 99b7ca76be fix: only skip the weighted slice for an icon-only last reaction
The previous attempt weighted every item, which made even Share collapse
to the left of its slice instead of pinning to the right edge.

Restore the natural-width carve-out for the last item, but gate it on
`!showCounter` — Share/Pay have no counter so they stay flush against
the right padding as before; Zap/Like/etc. become weighted when last so
the counter doesn't sprawl out to the edge and the row stays balanced.
2026-05-23 21:06:42 +00:00
Vitor PamplonaandGitHub 2ef738de14 Merge pull request #3039 from vitorpamplona/claude/fix-zaps-display-tHV2a
NIP-BC onchain zaps: add verification state machine & reverify driver
2026-05-23 17:04:22 -04:00
Claude e5b0755d9b fix: secondary external-signer login lands on onboarding when switching
LocalPreferences.setDefaultAccount called setCurrentAccount before
saveToEncryptedStorage. setCurrentAccount emits the new list onto the
savedAccounts MutableStateFlow, which AlwaysOnNotificationServiceManager
collects and reacts to by calling loadAccountConfigFromEncryptedStorage
for every saved account — including the just-added one. That call hit
encryptedPreferences(newNpub) before NOSTR_PUBKEY had been written, got
null, and cached the null in cachedAccounts.

cachedAccounts is a process-lifetime map, so the poisoned entry survived
the eventual disk write. Every subsequent switchUser to that account
took the cached null path, fell through to requestLoginUI(), and AccountScreen
rendered LoggedOffSetup — the onboarding screen with TOS unchecked, asking
the user to re-do the Amber handshake.

Write the per-npub file first, then seed the cache with the in-memory
AccountSettings, then publish onto the savedAccounts flow. Also stop
caching null returns in loadAccountConfigFromEncryptedStorage so any
future racy reader can't poison the cache either.
2026-05-23 20:16:13 +00:00
Claude 18fb75285e fix: keep reaction icons evenly distributed when Share is disabled
The reaction row gave every item except the last a `Modifier.weight()`,
which made the last item collapse to its natural width and hug the right
edge of the content area. With Share (icon-only) as the default last
item, all icons appeared evenly distributed.

When the user disabled Share, the last weighted slot moved to Zap. Zap
renders icon + counter, so its natural-width row took more space at the
right and pulled the rightmost icon away from where the other icons sat
(each at the left of a now-wider weighted slice), leaving the row
looking unbalanced.

Give every reaction an equal weighted slice so icons sit at the left of
their slice regardless of which reactions are enabled. The unused space
at the end of the last slice naturally provides the right-side padding
where Share used to sit.
2026-05-23 19:27:19 +00:00
Claude daa83959b6 refactor(onchain-zaps): extract verification coordinator from LocalCache
Moves the asynchronous chain-verification side of NIP-BC onchain zaps out of
LocalCache into a dedicated OnchainZapResolver class living alongside other
NIP-specific subpackages under model/nipBCOnchainZaps/. LocalCache shrinks by
~240 lines and now owns only the synchronous event-dispatch responsibility:
loading the event, attaching the optimistic UNVERIFIED entry for the sender's
own zap, and delegating the verifier launch to the resolver.

The resolver owns:
- launchVerification(event, source, repliesTo) — async fire-and-forget
- reverifyOnchainZapsForNote(note) — used by the gallery's screen-driven loop
- onchainTipHeightFlow — shared chain-tip poller, lazy + WhileSubscribed
- verifyingEventIds / reverifyingNoteIds — in-flight de-duplication
- reverifySemaphore — parallelism cap

OnchainZapGallery now calls LocalCache.onchainZapResolver.{reverifyOnchainZaps
ForNote, onchainTipHeightFlow} directly. consume(OnchainZapEvent) passes the
already-computed repliesTo into launchVerification so the new-event path
doesn't recompute it on the verifier side.

No behavior change — all 22 onchain-zap tests still pass.
2026-05-23 16:13:19 +00:00
Vitor PamplonaandGitHub fc5587f46f Merge pull request #3038 from nrobi144/feat/desktop-wallet-zapping
feat(desktop): wallet zapping, LNURL-pay send, QR receive, and session persistence
2026-05-23 12:05:56 -04:00
Vitor PamplonaandGitHub 0461072254 Merge pull request #3041 from vitorpamplona/claude/jolly-cray-6vKga
Makes the NWC process less strict, while checking for inconsistencies after the request reply is processed.
2026-05-23 12:05:09 -04:00
Vitor PamplonaandGitHub fc7813afbb Merge pull request #3036 from vitorpamplona/claude/affectionate-gauss-UWDJ4
Add NIP-82 Software Applications support with dedicated feed
2026-05-23 12:00:08 -04:00
nrobi144andClaude Opus 4.6 e4691f6d93 fix(desktop): remove obsolete ZapDialogLogicTest
Test referenced formatSats, DEFAULT_ZAP_AMOUNTS, and ZapType which
were removed/made private in upstream merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:45:54 +03:00
nrobi144andClaude Opus 4.6 2b14b77acf feat(desktop): support LNURL-pay and lightning addresses in send dialog
Rewrite SendDialog with sealed state machine that auto-detects input
type (BOLT11, LNURL bech32, lightning address). For LNURL/address:
resolves endpoint, shows amount form with min/max hint, optional
comment field, fetches invoice, then pays via NWC. Strips lightning:
URI prefix. Inline copiable errors with retry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:40:19 +03:00
nrobi144 a5405fef34 Merge remote-tracking branch 'upstream/main' into feat/desktop-wallet-zapping
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt
2026-05-23 15:19:47 +03:00
nrobi144andClaude Opus 4.6 4936d187fe fix(desktop): improve send/receive dialogs and LNURL error surfacing
SendDialog: switch to Dialog+Card with X close, inline copiable error
messages, button resets to "Pay Invoice" on error for retry.

LightningAddressResolver: return error body from callback responses so
server error messages (e.g. "Recipient wallet error") surface to user
instead of generic "Failed to fetch invoice". Also check "message"
field in addition to "reason" for error extraction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:17:08 +03:00
nrobi144andClaude Opus 4.6 00708d92d3 feat(desktop): redesign receive dialog with QR code and cleaner UX
Replace AlertDialog with Dialog+Card pattern. Invoice created state now
shows centered amount, description, 240dp QR code, and full-width
"Copy Invoice" button. Close via top-right X button. Input form gets
full-width "Create Invoice" button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:35:18 +03:00
nrobi144andClaude Opus 4.6 562cd3355b fix(desktop): fix feed cold-boot race and remove NWC diagnostic println
Add LaunchedEffect that rescans cache when followedUsers populates after
startup, fixing empty feed when contact list arrives after initial scan.
Remove diagnostic println from NwcPaymentHandler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:02:48 +03:00
nrobi144andClaude Opus 4.6 e690292bbd fix(desktop): fix nsec session not persisting across restarts
LoginScreen's fire-and-forget save coroutine used rememberCoroutineScope
which got cancelled when the composable left composition after login.
Move saveCurrentAccount() to onLoginSuccess in Main.kt which uses the
app-level scope that survives recomposition. Fixes both nsec login and
generate-new-account flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:02:36 +03:00
Vitor PamplonaandGitHub 4391fae915 Merge pull request #3037 from vitorpamplona/claude/pin-followed-chats-iSRJ9
feat: pin followed public chats to the top of the Public Chats feed
2026-05-22 20:00:53 -04:00
Claude 0313dcf3fa fix(onchain-zaps): clear second-audit findings
Addresses the 15 issues from the second audit pass. Key changes:

- Per-event resolution flag (`Note.onchainZapResolved`) replaces the unbounded
  rejection blocklist. The flag is set on terminal verifier verdicts
  (Confirmed or hard-Rejected) and gates the verifier launch in `consume()`.
  Travels with the Note so it clears on `removeAllChildNotes()`.

- Per-event in-flight set (`verifyingEventIds`) deduplicates concurrent
  verifier launches across `consume()` echoes and `reverifyOnchainZapsForNote`
  races. Solves: profile-only zaps bypassing the all-CONFIRMED guard,
  Rejected entries re-firing the verifier on every echo, and the
  consume()/reverify TOCTOU race.

- Per-note reverify gate (`reverifyingNoteIds`) prevents multiple visible
  galleries from launching concurrent reverify passes for the same note.

- `removeOnchainZapForSource` now refuses to remove a CONFIRMED entry — only
  an explicit fresh CONFIRMED replacement can change one. Prevents the
  cross-target downgrade where one target's transient ZERO_VERIFIED_AMOUNT
  erases a sibling target's already-confirmed entry. Also non-nullable
  pubkey parameter to close the null-vs-null comparison hole.

- `innerAddOnchainZap` dedup tightened: exact structural equality skips
  spurious flowSet invalidations on relay echoes, but same-level + equal
  verifiedSats from a DIFFERENT source now replaces (fixes multi-signer
  attribution lock-in).

- Tip flow uses explicit try/catch that re-throws CancellationException
  instead of `runCatching` (same fix the previous audit applied to the
  verifier). Lazy initializer falls back to a constant-null StateFlow if
  `Amethyst.instance` isn't initialized yet, instead of throwing.

- Gallery driver: unconditional first-view kick (no longer waits for the
  tip flow's first non-null emission), separate effect keyed on pending
  entry count so a fresh UNVERIFIED arrival kicks reverify immediately
  instead of waiting up to 60s for the next tip poll.

- `observeNoteZaps`'s memoization now keys on the `onchainZaps` map
  reference so lightning-zap traffic on the same note doesn't churn the
  onchain gallery.

- `reverifyOnchainZapsForNote` uses `supervisorScope` so a single failed
  verifier doesn't cancel its siblings, and the semaphore permits bump
  from 4 → 8 reduces head-of-line blocking when many galleries reverify
  concurrently.
2026-05-22 22:42:25 +00:00
Vitor Pamplona 585b28163a Better rendering of Public Chats 2026-05-22 18:25:07 -04:00
Vitor PamplonaandGitHub 2c8ed6c64f Merge pull request #3031 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-22 18:22:46 -04:00
Crowdin Bot a699920e96 New Crowdin translations by GitHub Action 2026-05-22 22:06:54 +00:00
Vitor PamplonaandGitHub 653ca7ce88 Merge pull request #3034 from nrobi144/feat/desktop-note-action-ux
feat(desktop): note action bar — long-press details popups + right-click customize
2026-05-22 18:05:22 -04:00
Vitor PamplonaandGitHub ae56a295d4 Merge pull request #3035 from greenart7c3/claude/epic-newton-OZLCC
Use URL SHA for Blossom bridge, not imeta hash
2026-05-22 14:53:00 -04:00
Claude 862dce27fe fix(blossom-bridge): always use URL sha, ignore imeta x
On resizing CDNs the imeta `x` (post-resize hash) can differ from the
`ox` (original hash) embedded in the URL. The bridge previously preferred
`explicitHash` over the URL's sha for "authoritative casing", but the
upstream file on `xs` is named after the URL's sha, not the imeta hash.
For URLs like https://image.nostr.build/<ox>.png with imeta x=<post-resize>
the cache would request /<x>.png and 404 on miss.

Always use the sha parsed from the URL path; drop the explicitHash
parameter. `extractSha256FromUrlPath` already lowercases, so the casing
concern is moot.
2026-05-22 16:23:34 +00:00
nrobi144andClaude Opus 4.6 3185df21b6 fix(desktop): reactive counters, quote boost, and boost detail popup
- Add kind 1 (replies) to interaction subscriptions
- Key count reads on FlowSet state for reactive updates
- Wire Quote menu item to ComposeNoteDialog with q-tag support
- Add BoostsPopup on long-press repost icon (who boosted)
- ComposeNoteDialog now accepts quoteOf param with nostr: URI pre-fill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 14:03:48 +03:00
nrobi144andClaude Opus 4.6 27543ac304 feat(desktop): long-press details popups + right-click customize for note actions
- Long-press zap icon → floating popup with zap receipts (sender, amount, message)
- Long-press like icon → floating popup with reactions grouped by emoji
- Right-click like icon → emoji picker (DropdownMenu with 6 common emojis)
- Right-click repost icon → Repost/Quote options (DropdownMenu)
- Right-click zap icon → custom zap dialog (preserved existing behavior)
- Long-press reply → opens thread (same as click)
- ActivePopup sealed class ensures only one popup open at a time
- Popup + ElevatedCard for rich content, DropdownMenu for option lists
- combinedClickable with explicit ripple preserves IconButton UX
- PopupProperties(focusable = true) for desktop click-outside dismiss
- @Immutable on ZapReceipt for Compose stability
- Note param added to NoteActionsRow, passed from FeedScreen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 07:00:31 +03:00
nrobi144andClaude Opus 4.6 746dab51b4 fix(desktop): fix NWC relay connection, disconnect crash, and balance error handling
- Remove premature ensureRelayConnected check — NostrClient connects
  on subscribe/publish via sendOrConnectAndSync
- Fix disconnect crash: use appScope instead of rememberCoroutineScope
  to survive recomposition when nwcConnection goes null
- Surface balance errors/timeouts as snackbars instead of silent swallow
- Add ensureRelayConnected helper to RelayConnectionManager
- Add Phase 2 embedded wallet research doc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 06:23:05 +03:00
Claude a338574f44 fix(nwc): surface rejected spoof replies in timeout message
When a kind-23195 event arrives signed by someone other than the wallet
service we sent the request to, we now count it on the pending entry and
leave the entry in place so the legitimate reply can still resolve. But
if no legitimate reply arrives and the 30s timeout fires, the user used
to see a generic "Wallet request timed out" — indistinguishable from
"the wallet is just slow", even when an active attacker was forging
replies and dropping the real ones.

Carry the per-request spoof count through to the timeout error message:

  - NwcPaymentTracker.PendingRequest gains an AtomicInteger spoofAttempts.
    onResponseReceived increments it on WrongAuthor.
  - New tracker method spoofAttemptsFor(requestId) reads the count.
  - Account exposes nwcSpoofAttempts() and cleanupNwcRequest() so the
    UI doesn't need to reach into LocalCache.
  - Account.sendNwcRequestToWallet now returns the request event id so
    callers can identify the pending entry.
  - WalletViewModel.launchTimeout takes a () -> HexKey? provider and
    fetches the spoof count when the timeout fires. The error becomes
    "Wallet request timed out — N replies were rejected because they
    were signed by an unexpected key. Your relay may be untrusted."
    Also calls cleanupNwcRequest on timeout to avoid leaking the entry.

Silent on the happy path: a forged reply followed by the real one does
not trigger any user-facing message — the spoof count is discarded with
the matched entry.
2026-05-21 22:29:55 +00:00
Claude b17bb8339e feat(nip82): render bundled assets inside the release card
Replace the placeholder "N assets bundled" line with a real list of
compact rows for each `e`-tagged Software Asset in the release.

Each row loads the referenced asset event id through
`LoadAssetNote` (uses LocalCache first, falls back to
`checkGetOrCreateNote` for ids never seen) and then
`observeNoteEvent<SoftwareAssetEvent>` — which both observes the
LocalCache flow and registers the note with `EventFinder` so the
relay round-trips the missing asset event. When the asset arrives
the row recomposes with MIME, version, optional variant, size,
platform chips, and a Download link to the asset url.

The standalone `RenderSoftwareAsset` card (kind 3063 in a feed or
thread) is unchanged; this only fills out the release detail view.
2026-05-21 21:41:52 +00:00
Claude 3f91cb1689 fix: actually fetch ChannelCreateEvent (kind 40) and widen the relay set
filterMissingChannelsById had an inverted isEmpty() check that emitted
zero filters, so kind 40 was never requested from any relay. Channels
discovered from kind 42 messages stayed as empty stubs unless the
creator also happened to publish a kind 41 metadata update findable on
the same relay — which is why most cards in the Public Chats feed
loaded with no name or picture.

Fix:
- Drop the inverted condition; mapOfSet guarantees non-empty values, so
  emit a RelayBasedFilter for every (relay, channelIds) entry.
- Widen the relay set per channel to include the user's search and
  indexer relay lists. Falls back to DefaultSearchRelayList /
  DefaultIndexerRelayList when those lists are empty.
- Plumb the Account through ChannelFinderQueryState so the assembler
  can read the search/indexer flows. Mirrors EventFinderQueryState.
2026-05-21 21:35:27 +00:00
Claude 73f1e6ae9c fix(onchain-zaps): harden against spoofing, fix re-verify lifecycle, audit cleanup
Addresses the 15 findings from the high-effort code review on top of the
optimistic-attach fix. Notable behavior changes:

- Per-source removal: `Note.removeOnchainZapForSource(txid, pubkey)` only
  drops an entry whose source matches, preventing a spoofed kind:8333 with
  the same txid but a bystander recipient from erasing a legitimate
  CONFIRMED entry. Rejected (txid, sender) pairs are recorded so a fresh
  event id from the same attacker no longer re-flickers into the gallery.

- Sender-only optimistic attach: only the user's own outgoing zap (relay ==
  null path) gets the optimistic UNVERIFIED entry. Incoming zaps render
  only after on-chain verification, so an attacker-controlled `amount` tag
  can't briefly mislead viewers. `claimedSats` is clamped >= 0.

- Reverification across every screen: the chain-tip poller moves from the
  thread screen into `LocalCache.onchainTipHeightFlow` (lazy, shared,
  WhileSubscribed). The onchain-zap gallery itself drives reverification
  whenever it composes with non-CONFIRMED entries — covers home feed,
  notifications, profile, channel and single-note views. The gallery
  observes the tip flow and the note's zap state, so new arrivals while
  the gallery is on screen are picked up too.

- Verifier fan-out + parallelism: re-arrivals skip the verifier launch
  when every target note already holds a CONFIRMED entry for the txid.
  `reverifyOnchainZapsForNote` now runs verifier calls in parallel,
  capped by a 4-permit semaphore.

- Monotonic upgrade based on explicit `OnchainZapStatus.level` instead of
  `ordinal`, with a unit test locking the order. Same-level entries with a
  larger `verifiedSats` are accepted so a stale indexer estimate isn't
  permanent.

- Cancellation propagation: `catch (Throwable)` rethrows
  `CancellationException` in `verifyAndUpgradeOnchainZap` so screen-scoped
  callers tear down cleanly.

- Memory visibility: `Note.onchainZaps` is `@Volatile` since the
  reverification driver reads it on Main while the IO scope writes.
2026-05-21 21:34:36 +00:00