Commit Graph
14116 Commits
Author SHA1 Message Date
Claude dc2c7f9be1 feat(amethyst): getFeedDigest verb — feed-summary surface for the LLM
New verb: getFeedDigest(hoursBack, maxNotes).

Use when the user asks "summarize my Nostr feed", "give me a digest
of what my follows posted today", "recap Nostr", or any other
summary / digest / recap intent.

Returns a structured snapshot for AI summary instead of a raw note
list: total note count, unique author count, top hashtags (≤10) and
top mentioned users (≤10) — with display names resolved from the
local kind:0 cache — alongside the trimmed note body. The LLM uses
the aggregate signals to write a one-paragraph "the conversation
focused on X, with N people posting about Y" instead of having to
re-derive frequencies from a raw list.

Implementation:
  * Shared core extracted into fetchFollowFeed(account, since, limit)
    so getRecentFromFollows and getFeedDigest don't duplicate the
    drain logic.
  * Over-fetches by 3× the visible cap so stats are computed over a
    larger sample than the LLM sees, capped at 500 events for bounded
    on-device work.
  * Hashtag bucketing: lowercases + strips leading #, so #Bitcoin
    and #bitcoin collapse.
  * Mention bucketing: skips self-mentions (some clients tag the
    author themself, not useful for the digest).

New @AppFunctionSerializable result types:
  * HashtagFrequency, MentionFrequency — count + identifier.
  * FeedDigestResult — windowHours, totalNoteCount, uniqueAuthorCount,
    topHashtags, topMentions, notes.

Total verb count: 22. app_metadata.xml updated so Gemini's tool
picker can pitch the summary surface specifically.

Known scope: currently returns kind:1 from the user's kind:3 follow
list — does NOT match the in-app home feed exactly. The home feed
includes reposts, long-form, polls, comments, etc., respects the
user's currently selected NIP-51 list, and filters muted users.
Aligning the digest to the home feed (via HomeNewThreadFeedFilter
against LocalCache) is a documented follow-up.
2026-05-26 15:58:30 +00:00
Claude 4617be2068 chore(amethyst): collapse unreachable NWC when branch
withTimeoutOrNull(deferred.await()) returns a flattened Response? —
both "timeout" and "wallet sent null" produce null, and we already
catch null via the elvis-return above. The explicit `null ->` arm in
the response switch was dead code; the compiler warned about it.
Folded the "wallet sent null we couldn't decrypt" case into the
timeout error message since they're indistinguishable to the caller.
2026-05-26 15:40:34 +00:00
Claude ee0942d555 Merge remote-tracking branch 'origin/main' into claude/brave-clarke-hJ0PK 2026-05-26 15:32:47 +00:00
David KasparandGitHub d1610bf976 Merge pull request #3048 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-26 16:24:44 +02:00
Crowdin Bot 153b9495da New Crowdin translations by GitHub Action 2026-05-26 14:21:22 +00:00
Vitor PamplonaandGitHub 9cd438570f Merge pull request #3050 from davotoula/fix/ios-build-failure
Complete Phase 2 KMP migration to unblock iOS CI
2026-05-26 10:19:16 -04:00
davotoula 28865f38c3 tests:
- cover CodePoints helpers and Channel.relays() equal-count behaviour
- Two new test files in commons/src/commonTest/, both run under :commons:jvmTest.
2026-05-26 15:53:37 +02:00
Claude d0f6739a30 feat(amethyst): NWC auto-pay for zapUser and zapEvent
Returning a BOLT11 invoice for the user to paste somewhere defeated
the point of "Gemini, zap Alice 21 sats". Now when the active account
has a Nostr Wallet Connect (NIP-47) wallet configured in Amethyst,
both zap verbs pay the invoice automatically over NIP-47 and report
the outcome inline.

Implementation:

  * payViaNwcOrNull(account, bolt11, zappedNote) — null when no NWC
    set up (caller falls back to manual). Otherwise wraps the
    callback-based Account.sendZapPaymentRequestFor in a
    CompletableDeferred + withTimeoutOrNull. 30s budget; if the
    wallet doesn't answer in that window the caller sees an
    nwcError of "wallet didn't respond within 30s" and still has
    the raw invoice to fall back on.

  * Decodes the wallet's response: PayInvoiceSuccessResponse carries
    the preimage, PayInvoiceErrorResponse carries a typed code +
    message, NwcErrorResponse covers transport-level errors, null
    means "couldn't decrypt the reply" (rare — wallet misconfigured
    or our signer rejected). Each case maps to a typed
    NwcOutcome the verbs can render.

  * ZapResult / ZapInvoice grow four fields: nwcAttempted, nwcPaid,
    nwcPreimage, nwcError. The invoice is still always returned so
    Gemini can show it as a manual-payment fallback when NWC isn't
    configured or rejects. zapEvent attempts each split independently
    — one wallet failure doesn't block the rest.

Kdoc updates note the NWC behavior so the LLM picks up "if NWC is
configured, this just works" — that's the user-visible promise of
asking Gemini to tip someone.
2026-05-26 13:37:05 +00:00
davotoula 771ba67f31 Code review:
- guard shared NSDateFormatter in formattedDateTime iOS actual
2026-05-26 14:53:38 +02:00
Vitor PamplonaandGitHub bb921ad643 Merge pull request #3049 from nrobi144/feat/desktop-rich-text-and-profile
feat(desktop): rich text migration, profile metadata, copy JSON, @mention autocomplete
2026-05-26 07:55:55 -04:00
davotoulaandClaude Opus 4.7 5ec60e285b fix(commons): unblock :commons iOS compile after Phase 2 target flip
PR #3047 enabled iosArm64 + iosSimulatorArm64 on :commons and added
:commons:compileKotlinIosSimulatorArm64 as a CI gate, but the Phase 2
migration was incomplete — JVM-only APIs survived in commonMain and
several expect declarations had no iOS actual. Every main CI run since
the merge failed at "Compile Commons for iOS".

Migrations in commonMain
- Dispatchers.IO: add `import kotlinx.coroutines.IO` to 16 files, matching
  the quartz/NostrClient.kt pattern (kotlinx-coroutines 1.11 exposes IO on
  Native via this import; no shim needed).
- synchronized {}: replace with the existing KmpLock + withLock in
  EOSECache, AcceptedGamesRegistry, EventDeduplicator, ThumbHashDecoder,
  PeerSessionManager. Restructure two PeerSessionManager methods that
  late-init vals from inside the lock — withLock returns a tuple now.
- Unicode code points: drop java.lang.Character / String.codePointAt /
  String.offsetByCodePoints. Add commons/util/CodePoints.kt with surrogate
  -pair-aware KMP helpers; rewrite EmojiCoder + EmojiUtils against them.
- Byte<->String: encodeToByteArray() / decodeToString() / concatToString()
  in EmojiCoder, Base83, BlurHashEncoder, RobohashAssembler,
  LongFormPublishAction (drops Charsets / String(CharArray) / toByteArray
  no-arg).
- Math.round → Double.roundToLong in BlurHashEncoder.
- String.format → Compose Resources stringResource(res, vararg) overload
  in LoadingState (FeedErrorState).
- toSortedSet → sortedByDescending { }.mapTo(LinkedHashSet) in Channel —
  preserves the descending-by-relay-count iteration order callers depend
  on.
- Comparator<T>: kotlin.Comparator on Native takes non-null T. Align
  CreatedAtComparator / CreatedAtComparatorAddresses to compare(a, b) and
  drop dead null checks in CreatedAtIdHexComparator.

iOS actuals (commons/src/iosMain/)
- WeakReference: switch from typealias to explicit `actual class`. The
  expect param is `referent` (matches java.lang.ref); kotlin.native.ref.
  WeakReference uses `referred`, so typealias fails the expect/actual
  name-match check on Native. Add @file:OptIn(ExperimentalNativeApi).
- PlatformImage: functional IntArray-backed actual (used by BlurHash and
  ThumbHash decoders at runtime); Phase 3 will swap to CGImage.
- ChessDismissedGamesStorage: in-memory only; NSUserDefaults wiring lands
  with iosApp in Phase 3.
- SecureKeyStorage: stub throwing SecureStorageException. Keychain
  Services binding is Phase 4 per the iOS plan.
- formattedDateTime: NSDateFormatter with "yyyy-MM-dd-HH:mm:ss" + POSIX
  locale + local time zone (semantically matches the JVM
  DateTimeFormatter "uuuu-MM-dd-HH:mm:ss" for post-1970 timestamps).
- checkNotInMainThread: no-op (mirrors jvmMain).
- PlatformNumberFormatter: NSNumberFormatter(.DecimalStyle), with
  NSNumber.numberWithLongLong to disambiguate the NSNumber(Long)
  overload set.
- isDebug: false constant; iosApp can flip via Swift `DEBUG` flag later.

Verified locally
- :commons:compileKotlinIosSimulatorArm64 + compileKotlinIosArm64 green
- :quartz:iosSimulatorArm64Test green
- :commons:jvmTest + :quartz:jvmTest green (no JVM regression)
- :quartz:verifyKmpPurity + :commons:verifyKmpPurity + spotlessCheck green

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:38:01 +02:00
davotoulaandClaude Opus 4.7 027808ae54 skills(find-missing-translations): filter out keys Crowdin already owns
A "missing" key in values-<locale>/strings.xml is not always actionable:
Crowdin omits source-identical translations on export (translator chose
"use English" for brand terms like "Nowhere X", loanwords like "Apps",
or version prefixes like "v%1$s"). Adding source-identical fallbacks
locally is noise that the next Crowdin sync strips again; Android already
falls back to values/strings.xml at runtime.

Add a Step 2.5 sync-timestamp filter that uses the latest
"New Crowdin translations by GitHub Action" commit reachable from HEAD as
the cutoff. Keys added to values/strings.xml after that commit are
genuinely new (Crowdin hasn't exported them yet); anything older is
Crowdin's responsibility. The reachable-from-HEAD check survives the
common workflow of deleting the l10n_crowdin_translations branch after
merging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:20:51 +02:00
Claude 5efb5d90e5 feat(amethyst): zap verbs + LLM-friendly kdocs + Gemini discovery plan
Three deliverables:

1) Two new write verbs:
   * zapUser(user, sats, comment?) — builds the NIP-57 kind:9734
     profile zap and fetches a BOLT11 invoice from the recipient's
     Lightning service. Returns the invoice — caller pastes into a
     Lightning wallet (no NWC auto-pay yet). 21 sats default,
     1M sats cap, 280-char comment cap.
   * zapEvent(eventId, sats, comment?) — same but for a specific
     note, with full NIP-57 zap-split support via
     ZapActions.buildEventZapRequestsForSplits. Returns one invoice
     per recipient when the post carries `zap` tags.

   Total verb count: 21 (8 read for feeds/profiles, 3 read for
   identity / followers, 4 read for inbox/zaps/streams, 4 write
   for note/follow/unfollow/dm, 2 write for zaps).

2) Reworked every verb's kdoc first sentence into an LLM-friendly
   "use when..." trigger phrase. Gemini's tool picker matches user
   queries against the descriptions (we generate them via
   @AppFunction(isDescribedByKDoc = true)) — phrasing like "Find a
   person on Nostr by name. Use when the user wants to look someone
   up..." gives the model concrete prompts to recognise instead of
   internal NIP names.

   Affected: searchProfiles, getRecentFromFollows, getNotesByUser,
   getProfile, searchByHashtag, getActiveAccountInfo, getRecentDms,
   getZapsReceived, postNote, followUser, unfollowUser, sendDm,
   zapUser, zapEvent.

3) amethyst/plans/2026-05-26-appfunctions-gemini-discovery.md —
   verification protocol for testing on-device whether Gemini's
   tool picker actually surfaces our verbs from natural-language
   prompts. Includes specific test prompts mapped to expected
   verbs, fallback diagnostics (clear AppSearch + restart), and
   the conditions under which it'd be worth defining our own
   @AppFunctionSchemaDefinition namespace.

Plus minor: comment parameters switched to nullable (String? = null)
because KSP rejects non-nullable types with defaults.
2026-05-26 00:01:13 +00:00
Claude 8e1b31a550 feat(amethyst): four write verbs for Gemini — postNote, follow, unfollow, sendDm
Phase 4 from the signer-prompt plan, scoped to Option B (refuse NIP-55
with a typed NotSupportedException). Internal-key and NIP-46 bunker
accounts can now publish from Gemini.

New @AppFunction methods:

  * postNote(text) — kind:1 short text note. Caps at 8000 chars to
    catch accidentally-pasted documents; publishes to outbox relays
    with per-relay ack reported.

  * followUser(user) / unfollowUser(user) — kind:3 contact list
    update via FollowActions. Detects already-following / not-
    following and returns WriteResult.unchanged() rather than
    re-publishing the same kind:3. New follows stamp the relay hint
    from the target's cached kind:10002 write list, mirroring
    User.bestRelayHint().

  * sendDm(recipient, text) — NIP-17 gift-wrap via DmActions.buildTextDm.
    Resolves per-recipient relay set through DmActions.resolveDmRelays
    (permissive mode — falls back through NIP-65 read to bootstrap so
    Gemini users don't trip on the strict kind:10050 rule). Returns
    one DmDelivery per wrap (recipient + sender's own copy).

Signer gating — requireInProcessSigner():
  * Read-only signers (npub-only login) → AppFunctionNotSupportedException
    "sign in with a private key or NIP-46 bunker to publish".
  * NIP-55 external signers (Amber) → AppFunctionNotSupportedException
    "open Amethyst directly to complete the action". Detected via
    qualified class name to avoid hard-coupling the bridge to the
    nip55AndroidSigner module.
  * NostrSignerInternal / NostrSignerRemote — sign in-process; the
    NIP-46 round-trip already suspends through .sign(), no special
    handling needed.

New @AppFunctionSerializable types:
  * WriteResult — { changed, eventId?, publishedTo, rejectedBy }
  * SendDmResult — { messageEventId, deliveries: List<DmDelivery> }
  * DmDelivery — { recipientNpub, recipientPubkeyHex, wrapId,
                   publishedTo, rejectedBy, relaySource }

All 19 verbs now registered in the generated dispatcher (15 read + 4
write). app_metadata.xml updated so Gemini's tool picker pitches the
broader surface, including the NIP-55 caveat.
2026-05-25 23:41:58 +00:00
Vitor PamplonaandGitHub e93492f491 Merge pull request #3047 from vitorpamplona/claude/zealous-mendel-TK91O
Phase 1: iOS support for Quartz and Commons (KMP purity)
2026-05-25 19:38:51 -04:00
Claude ac105ca2f3 chore(amethyst): enrich AppFunction outputs so Gemini can name names
Every verb that returned a pubkey now also returns the best-effort
display name from the local kind:0 cache. Before this commit Gemini
could only say "you got a DM from npub1abc…" — now it can say
"you got a DM from Alice" because the LLM has the field at hand
instead of having to chain another lookup.

  * NoteHit gains authorDisplayName (cache-resolved, null when the
    author's kind:0 isn't local yet). Applied to every verb that
    returns notes: searchNotes / getRecentFromFollows / getNotesByUser /
    searchByHashtag / getMyRecentNotes / getMyMentions /
    getRepliesToNote / searchArticles.

  * DmMessage gains fromDisplayName + sentByMe — the latter lets
    the caller distinguish "Alice said X" from "I said Y" when both
    appear in the same thread snapshot.

  * LiveStreamHit gains streamingUrl (was missing entirely — without
    it the verb is useless, you can't watch a stream you can't open)
    plus hostDisplayName.

  * getProfile cache-hit path now actually populates `about` — was
    silently null before because the early-return branch didn't read
    it out of UserInfo. Cache-miss path was always correct.

Implementation: one `displayNameOf(HexKey): String?` helper reads from
Amethyst.instance.cache (LocalCache) — the same cache the foreground
UI uses. Zero allocations beyond the lookup, no network round-trip.
2026-05-25 23:11:10 +00:00
Claude c10ed49631 feat(amethyst): Tier 2+3 read-only Gemini verbs — 15 total
Now exposing the full read-only Nostr surface to Gemini. Seven new
@AppFunction methods on top of the previous eight:

  * getMyRecentNotes(limit) — author=me filter on kind:1.

  * getMyMentions(limit) — p-tag=me filter on kind:1. "Did anyone @ me?".

  * getRepliesToNote(eventId, limit) — e-tag=eventId filter on kind:1.
    Pair with getMyRecentNotes(1) for "did anyone respond to my last post?".

  * getZapsReceived(hoursBack) — drains kind:9735 receipts addressed to
    the user in the window, parses the bolt11 invoice from each, sums
    sats. Returns total + zap count + unique zappers + count of
    receipts whose bolt11 was unparseable.

  * getRecentDms(peer?, hoursBack, limit) — NIP-17 gift-wrap drain +
    unwrapAndUnsealOrNull decrypt. kind:14 text DMs only for v1 (skip
    kind:15 encrypted-file headers to keep payloads bounded). Widens
    the `since` filter by 2 days for NIP-59's randomised-past
    created_at trick, then trims back to the requested window.

  * searchArticles(query, limit) — same as searchNotes but kind:30023
    long-form articles. Content snippet truncated at 2000 chars so a
    book-length article doesn't blow up the AppFunctions response;
    Gemini can ask the user whether to fetch the full article via a
    different verb.

  * getLiveStreams(limit) — NIP-53 kind:30311 with status=live (uses
    quartz's 8-hour staleness guard via LiveActivitiesEvent.isLive).
    Returns title, summary, host npub, start time, event id.

Plus updated res/xml/app_metadata.xml so Gemini's tool picker pitches
the full surface to users.

KSP-verified — 15 verbs total in the generated dispatcher:

    getActiveAccountInfo  getFollowing            getLiveStreams
    getMyMentions         getMyRecentNotes        getNotesByUser
    getProfile            getRecentDms            getRecentFromFollows
    getRepliesToNote      getZapsReceived         searchArticles
    searchByHashtag       searchNotes             searchProfiles

Write verbs (post, follow, zap, sendDm) still deferred behind the
signer-prompt plan in amethyst/plans/2026-05-25-appfunctions-signer-prompts.md
— no behavior change there.
2026-05-25 23:06:47 +00:00
Claude 5c46d0a7b3 feat(amethyst): five more read-only Gemini verbs — full Tier 1 read surface
After the on-device round-trip proved the AppFunctions plumbing works,
adding the verbs that make Gemini actually useful for a Nostr user.
All read-only, no signer interaction, all build on existing actions /
Account state.

  * getRecentFromFollows(limit) — "what's happening on Nostr today?"
    Drains recent kind:1 from people the user follows; same relay set
    the home-feed UI uses (account.homeRelays).

  * getNotesByUser(user, limit) — "what did Vitor post recently?"
    Accepts npub or 64-hex. Prefers the target's NIP-65 write relays
    when cached, falls back to the active account's home relays.

  * getProfile(user) — "who is npub1xq5...?". Cache-first via
    LocalCache; falls back to a short network drain for unseen users.
    Returns GetProfileResult{found, profile} so callers know whether
    the user just isn't in cache or doesn't have a kind:0 yet.

  * searchByHashtag(hashtag, limit) — "find Nostr posts about Bitcoin".
    NIP-12 `t` tag filter, lowercased to match the client convention.

  * getActiveAccountInfo() — "who am I logged in as?" Diagnostic verb
    returning npub, display name, follow count, outbox + DM relay
    counts. Distinguishes signed-in from signed-out via a flag rather
    than a magic empty result.

Plus:
  * decodeUserOrThrow helper for npub/hex parsing, throws
    AppFunctionInvalidArgumentException with a typed message so
    callers see "expected npub1… or 64-char hex" instead of a stack.
  * TextNoteEvent.toNoteHit helper — extracted from the existing
    searchNotes path to avoid duplication.
  * Updated res/xml/app_metadata.xml description so Gemini's tool
    picker can pitch a broader summary to the user.

KSP-verified: $AmethystAppFunctions_AppFunctionInvoker now dispatches
all eight verbs (the three from the previous commits plus these five).
2026-05-25 22:58:30 +00:00
Claude 0619788644 fix(amethyst): supply app_metadata so AppFunctions discovery actually works
After fixing the missing aggregated XML, Pixel 8 logcat still showed:
  D AppFunctions: Unable to resolve AppFunctionMetadata.

Comparing against Google's FilipFan/AppFunctionsPilot sample turned up
a separate metadata pointer the system requires:

  <property
      android:name="android.app.appfunctions.app_metadata"
      android:resource="@xml/app_metadata" />

This goes on the <application> element (not the service) and points to
an XML resource — distinct from the asset-side `app_functions.xml`
that the library auto-merges onto the service. The asset metadata
declares "here are my function ids and schemas"; the resource
metadata gives the agent a user-facing summary like "Search Nostr and
read your follows" to show users before they grant access.

Without the resource, the system can find our service and our
function list but can't resolve the descriptive metadata it shows
the user — so Gemini's tool picker stays empty.

Two new files:
  * amethyst/src/play/res/xml/app_metadata.xml — short description +
    displayDescription. Update when the @AppFunction surface grows.
  * play AndroidManifest <property> pointing at the resource.

Also dropped our explicit <service> declaration for
PlatformAppFunctionService — confirmed via the appfunctions-service
AAR that the library auto-merges that exact entry, complete with
permission + intent-filter, so our copy was redundant.
2026-05-25 22:03:46 +00:00
Claude 82188b719a fix(amethyst): generate app_functions.xml so the system can resolve metadata
The androidx.appfunctions-compiler runs in per-module mode by default,
emitting only the dispatcher Kotlin code. The aggregator that builds
the `app_functions.xml` + `app_functions_v2.xml` assets is gated behind
a KSP argument that was off.

Symptom on a Pixel 8 running our APK:
  D AppFunctions: Unable to resolve AppFunctionMetadata.

Without the aggregated asset, the manifest's
`android.app.appfunctions` property pointed at a file that didn't
exist; the System UI couldn't enumerate our @AppFunction methods so
Gemini's tool picker never saw them.

Setting `appfunctions:aggregateAppFunctions = "true"` on the amethyst
module turns the aggregator on. Verified post-build:
  assets/app_functions.xml           (688 bytes — manifest pointer + ids)
  assets/app_functions_v2.xml        (19.7 KB — full schemas + kdoc descriptions)
Both list searchProfiles / searchNotes / getFollowing with the kdoc
descriptions Gemini will render.

Library modules (commons/quartz) would set this to "false" — only the
final app emits the aggregate. We don't currently apply the KSP plugin
in any library module, so this is the only place that matters.
2026-05-25 21:50:56 +00:00
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
David KasparandGitHub 97bb6a5720 Merge pull request #3046 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-25 21:04:55 +02:00
Crowdin Bot 61372313fd New Crowdin translations by GitHub Action 2026-05-25 18:55:58 +00:00
davotoulaandClaude Opus 4.7 46c46aa69e i18n: translate missing cs/de/sv strings (apps, on-chain zap, legal, NIP-82)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:50:56 +02:00
nrobi144andClaude 64293f2fcc fix(desktop): fix text spacing in rich text viewer
Pure text paragraphs now render as a single Text composable instead
of individual words in FlowRow, eliminating unwanted inter-word gaps.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-25 09:50:43 +03:00
nrobi144andClaude d6875a144b feat(desktop): add @mention autocomplete to compose dialog
Type @ followed by a name in the compose/reply dialog to see a
dropdown of matching users from the local cache. Selecting a user
inserts their nostr:npub reference. Shows avatar + display name +
truncated npub.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-25 09:35:46 +03:00
nrobi144 76fdd5d25f Merge upstream/main into worktree-desktop-low-hanging-fruit 2026-05-25 06:45:44 +03:00
nrobi144andClaude 4d532e0698 fix(desktop): wire onHashtagClick, add RTL support, add identity fields
- Add onHashtagClick param to FeedNoteCard for hashtag navigation
- RTL paragraph alignment in DesktopRichTextViewer
- External identities (Twitter, GitHub, Mastodon) on profile card

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-25 06:43:21 +03: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 95ca12231c feat(amethyst): two more read-only Gemini verbs + signer-prompt plan
Read-only surface for the Gemini bridge now covers profiles, notes, and
the active account's follow set:

  * searchNotes(query, limit) — NIP-50 search over kind:1 short text
    notes via SearchActions.searchNotesFilter + INostrClient.fetchAll.
    Returns NoteHit list (eventId, npub, content, createdAt). alpha09
    of androidx.appfunctions doesn't support List<Int> parameters, so
    no caller-configurable kinds — kind:1 only for now.

  * getFollowing(limit) — reads account.kind3FollowList.userList.value
    (already resolved through LocalCache) and projects to FollowedUser
    with display-name / nip05 / picture from cached kind:0. Reports
    totalFollowing so the caller knows when limit truncated the list.

Both verbs follow the searchProfiles pattern: snapshot active account +
client at entry, never re-query sessionManager during the dispatch.

Plus amethyst/plans/2026-05-25-appfunctions-signer-prompts.md —
design plan for write verbs. Three signers (Internal / Remote /
External), three different latency + interaction models. Concrete
proposal: Internal first via postNote pilot, Remote as a follow-up,
External via PendingIntent (Option A) or NotSupportedException
(Option B — recommended for v1) depending on what bundle keys the
system shell respects. Open questions enumerated so the experiment
day is bounded.
2026-05-25 00:18:31 +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 31cfb53b25 feat(commons): extract NIP-17 DM verbs into shared actions package
Fourth verb extraction alongside FollowActions / SearchActions /
ZapActions. Closes the largest remaining amy-expert "thin assembly"
violation in cli/.

Two pieces moved out of cli/.../DmCommands.kt into commons:

  * DmActions.resolveDmRelays applies the strict-kind:10050 → NIP-65-
    read → bootstrap fallback policy the in-app flow uses. Returns a
    DmRelaySet with a typed RelaySource (KIND_10050 / NIP65_READ /
    BOOTSTRAP / NONE) so callers can surface the source — amy emits
    it on stdout, a future Gemini adapter could mention it in the
    assistant response.

  * DmActions.buildTextDm / buildFileDmReference are thin wrappers
    over NIP17Factory.createMessageNIP17 / createEncryptedFileNIP17
    that build the kind:14 / kind:15 template and gift-wrap in one
    call. Matches the FollowActions / ZapActions builder shape.

amy's DmCommands is now genuinely thin assembly: requireUserHex,
flag plumbing, call DmActions, render JSON. The 583-line file shrank
slightly and — more importantly — no longer carries NIP-17 logic
the rest of the codebase needs to look at.

Receive-side decrypt loop (3 lines of unwrapAndUnsealOrNull) stays in
amy; too small to extract and tightly coupled to amy's per-relay
attribution.

10 new tests for DmActions: strict/permissive fallback chain, null
recipient lists, RelaySource enum stability, and a smoke test that
buildTextDm produces a kind:14 with the right wrap count (sender +
recipient).
2026-05-24 23:45:33 +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 86434ea550 refactor(amethyst): use INostrClient.fetchAll instead of inlining the drain loop
quartz already ships an extension that does exactly what
AmethystAppFunctions.drain reimplemented — subscribe with the given
filters, collect events until every relay sends EOSE / closed /
cannot-connect or the timeout elapses, unsubscribe, dedup by id,
return sorted newest-first. See
quartz/.../relay/client/accessories/NostrClientFetchAllExt.kt.

Replacing the local drain with `client.fetchAll(filters, timeoutMs)`
trims 70+ lines of subscription listener boilerplate and gives the
adapter the same behavior the rest of the codebase already trusts.

amy's Context.drain stays — it adds per-event signature verification
and persistence to the file event store (the trust boundary for amy)
that fetchAll doesn't do.
2026-05-24 23:16:12 +00:00
Claude b150556f1e refactor(amethyst): drop PlayAmethyst — appfunctions doesn't need it
The previous wiring forced Amethyst to be `open`, added a 30-line
PlayAmethyst subclass that only implemented AppFunctionConfiguration
.Provider, and used tools:replace="android:name" in the play manifest
to swap classes. The justification was that the appfunctions runtime
discovers @AppFunction host classes via Application.appFunctionConfiguration.

Reading the KSP-generated dispatcher
($AmethystAppFunctions_AppFunctionInvoker.kt) shows that's only half
true. The invoker passes a default-construction fallback lambda when
instantiating the host class, and ConfigurableAppFunctionFactory takes
that fallback as a constructor argument. Provider is only consulted to
*override* construction — required for classes with non-default
constructors, optional otherwise.

AmethystAppFunctions has a no-arg constructor, so:
  * PlayAmethyst is deleted entirely
  * Amethyst goes back to `class Amethyst : Application()` (no `open`)
  * Play manifest reverts to plain `android:name=".Amethyst"`, no
    tools:replace gymnastics

Verified by assemblePlayDebug (APK builds clean) and the merged play
manifest still pinning the appfunctions service. If we ever add a
host class with constructor parameters (an Account-injected one, say),
we'll need to add Provider back — kdoc on AmethystAppFunctions
documents that.
2026-05-24 23:10:37 +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 29236d7801 chore(commons,cli,amethyst): three correctness wins + caller-responsibility kdoc
Closes the remaining items from the comparative review of the extracted
actions against the in-app Amethyst flows. All small, all surfaced by the
review.

  * amy follow now stamps the relay hint on new contact-list `p` tags.
    Best-effort read from the target's cached kind:10002 advertised
    relay list (first writeRelaysNorm). Mirrors User.bestRelayHint() —
    follows added via amy no longer have empty relayUri.

  * amy search user now dedups by pubkey (sorted newest-first) instead
    of by event id, matching the App Functions adapter. Multiple relays
    surfacing different kind:0 revisions for the same author collapse
    to one hit.

  * AmethystAppFunctions.searchProfiles captures the active account AND
    the relay client at function entry, then never touches sessionManager
    or Amethyst.instance again during the drain. Closes the account-
    switch race surfaced in the review.

  * FollowActions / SearchActions / ZapActions kdoc now lists the
    caller-side responsibilities each builder leaves to the consumer
    (publish, writeable check, relay hint, pseudo-kind filtering,
    LN round-trip, receipt verification, etc.). Documents the design
    rather than letting it leak through reviews.
2026-05-24 21:35:49 +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 54b09ea6e2 fix(commons): split-aware zap requests stop misrouting funds on multi-party notes
The previous ZapActions.buildEventZapRequest signed a single zap request
to a single recipient. Notes carrying NIP-57 zap-split tags, NIP-53
live-activity host tags, or NIP-89 app-definition metadata expect the
payment to be distributed across multiple parties — so `amy zap event`
silently overpaid one party and underpaid the rest. The correctness
review on the action-set flagged this as the only real bug in the
extracted verbs; this commit fixes it.

  * ZapSplitResolver — new commonMain object mirroring the resolution
    order in ZapPaymentHandler.kt (splits > live-activity hosts > app
    metadata > author fallback). Pure logic; pubkey→LN-address lookup
    is passed in as a suspend lambda so amy reads from its file store
    and Android reads from LocalCache, no shared cache-coupling.

  * ZapActions.buildEventZapRequestsForSplits — high-level helper that
    composes the resolver with per-share LnZapRequestEvent signing.
    Each request's `relays` tag unions sender + author + recipient
    inbox relays so the kind:9735 receipt routes to every interested
    party (matches signAllZapRequests in the Android handler).

  * amy zap event — rewired to the split-aware path. JSON output now
    enumerates each recipient with its share, LN address, request id,
    and BOLT11 invoice (or per-recipient invoice_error). Profile zaps
    (amy zap user) keep the simple single-recipient path since they
    have no split tags.

Tests: 12 new cases — LN-address splits, weighted pubkey splits, author
fallback, drop-silently-on-missing-LN, relay unioning, share rounding.
All 41 action tests green; both Android flavors compile.
2026-05-24 21:10:30 +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 17cee60aac feat(amethyst): expose searchProfiles to Gemini via androidx.appfunctions
First Phase 2 verb wired through to the Android App Functions runtime so
Gemini (and other system agents) can drive Amethyst.

Scope is intentionally narrow:
  * One read-only verb (searchProfiles), built on top of the existing
    SearchActions in commons. No write verbs yet — they need a story
    for NIP-46 / NIP-55 signer prompts from a background dispatcher.
  * Play channel only. appfunctions 1.0.0-alpha09 is a Google AI alpha;
    F-Droid builds continue to ship without any Google AI dependencies.

Architecture:
  * AmethystAppFunctions — plain Kotlin host with @AppFunction methods.
    The KSP-driven appfunctions-compiler discovers them and generates
    the dispatch metadata XML at build time.
  * PlayAmethyst — play-only Application subclass implementing
    AppFunctionConfiguration.Provider; supplies the factory the
    library uses to construct the host class. Manifest replaces
    android:name in the play flavor only; F-Droid keeps the unmodified
    Amethyst class.
  * The androidx-provided PlatformAppFunctionService is registered in
    the play manifest as the bind point — Amethyst doesn't ship a
    custom Service.

KSP is now a project-wide plugin (apply false at the root); applied in
amethyst/ to run the appfunctions-compiler over the play sourceSet.

Amethyst becomes `open class` so PlayAmethyst can extend it. No other
behavior change.
2026-05-24 18:23:59 +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
Vitor PamplonaandGitHub 2450955478 adjusting format 2026-05-24 13:58:57 -04:00
Vitor PamplonaandGitHub 07d77e1363 Merge pull request #3045 from vitorpamplona/claude/stoic-turing-TTiSd
Restructure privacy policy and add build-variant legal UI
2026-05-24 13:52:09 -04:00
Claude 6f8f5f7d57 docs: tighten PRIVACY.md — concise, truthful, lower-liability
Rewrites the policy/terms doc with three goals:

1) **Concise & easier to read.** Plain English, short sentences,
   removed redundant intros (the "How Amethyst Works (and Why That
   Matters Here)" block restated the Privacy intro), merged the
   "Visibility" + "Permanence" sections into one paragraph, and
   collapsed the Child Safety POC section into a single contact
   block near the top of the document.

2) **More truthful.** Two corrections:
   - F-Droid build uses UnifiedPush for notifications, not FCM. The
     previous text only mentioned Google Firebase Cloud Messaging,
     which was inaccurate for the F-Droid distribution.
   - Replaced "We rely on Google Play's age verification to make sure
     the user downloading the app is an adult" with "Amethyst's Google
     Play listing is rated 17+. The app does not request or store age
     information." Google Play does not actually verify user age, so
     the old wording overstated the protection.

3) **Lower liability.** Several specific changes:
   - Dropped the "We aim to acknowledge child-safety reports within
     72 hours" service-level commitment that the solo developer cannot
     reliably meet.
   - Softened "we will recommend that the offending relay be removed"
     and "What we can do: acknowledge the report, forward..." to
     discretionary "may forward" / "may stop recommending" phrasing.
   - Removed the absolute "data is strictly confidential and cannot
     be accessed by other apps" guarantee. Replaced with the narrower,
     verifiable claim that other apps cannot read app-local storage
     on a standard, non-rooted Android device.
   - Narrowed "Amethyst is built and distributed to comply with
     applicable child safety laws and regulations" to "Amethyst is
     distributed under Google Play's Child Safety Standards policy and
     applicable law" — same in spirit, smaller surface for dispute.

Content that the Google Play Child Safety Standards checklist
requires is unchanged: explicit CSAE prohibition, child-safety point
of contact (amethyst@vitorpamplona.com), in-app feedback mechanism
(Report Post / Report Account / Block Post / Block Account / Block
Relay / Mute), method for addressing CSAM (in-app report → block
relay → NCMEC/INHOPE → optional developer notice), compliance
statement, and references to the app name "Amethyst" and the Google
Play publisher "Vitor Pamplona". The F-Droid carve-out also remains:
the MIT License in LICENSE is identified as the only instrument
governing source-built distributions, with no additional terms.
2026-05-24 16:50:45 +00:00