Commit Graph
17298 Commits
Author SHA1 Message Date
Claude 4cf86ca030 docs(nip29): add Tier E — relay-behavior resilience + third-party conformance
The friendly-geode tiers test the client against a compliant relay; they neither
harden it against buggy relays nor surface bugs in the third-party NIP-29 relays
users connect to. Tier E adds both:

- E1: a deterministic fault-injecting mock relay (ignore since/until, short pages,
  echo-newest, out-of-order, EOSE-early/none, AUTH-CLOSE, mid-stream drop, caps)
  asserting the tail/pager still converge with no miss / loop / hang. Notes that
  UntilLimitPagingRelayTest + BackwardRelayPagerTest already cover several cases
  for the generic pager, to be extended to the NIP-29 #h shapes.
- E2: a conformance run against a real containerized NIP-29 relay via relayBench's
  RelayUnderTest adapter (AUTH gating, relay-signed 39xxx, previous-tag rejection,
  since/until semantics + caps) — a failure is a relay bug, doubling as a
  conformance report for the operator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 16:04:07 +00:00
Claude c8362fbc18 docs(nip29): per-screen × per-assembler test plan for the group-chat refactor
A validation plan an AI (or human) on this branch runs before trusting the
state-vs-content refactor. Organized by screen and covering every NIP-29
assembler (JoinedState, JoinedChatTail, OpenChatTail, OpenChatHistory,
OpenThreads, CardWarmup, RelayGroupsOnRelay, RelayGroupsDiscovery, the
ChannelPublic relay-group metadata branch, and the notifications path).

Tiers: (A) build + JVM suite; (B) new amethyst unit tests for each assembler's
filter shape + reconnect stability; (C) amy + geode/`amy serve` integration for
the framework, ingest and the can't-miss-messages paging property (C3); (D)
Android per-screen scenarios with logcat pass criteria, including the original
join-mid-session bug (D1) and reconnect no-replay (D8). Flags what is verifiable
headless vs. needs a device, and the known-failing serving-relay hazard row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 15:16:36 +00:00
Claude ee5dc25414 refactor(nip29): finish the datasource rename — Open threads + consistent filter-builder names
Continues the scope+role naming pass over the group-chat datasource layer:

- RelayGroupThreadFeed -> RelayGroupOpenThreads: it's the open group's threads
  tab, so it now sits in the "Open" family beside RelayGroupOpenChatTail/History.
- filterMetadataToRelayGroup -> filterRelayGroupState: matches the "state"
  concept (39000-39005 + pinned-id back-fill) the always-on state sub is named for.
- makeRelayGroupsDiscoveryFilter -> filterRelayGroupsDiscovery: consistent
  `filter…` verb with the rest of the relay-group filter builders.

The RelayGroupsOnRelay / RelayGroupsDiscovery assemblers keep their PLURAL prefix
on purpose: it marks the "browse/discover many groups" surfaces apart from the
singular in-a-specific-group ones (RelayGroupJoined*/Open*/Card*). The already-
consistent filterRelayGroupsBy* discovery builders are left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 14:33:56 +00:00
Claude 54b2b42d87 refactor(nip29): scope Card warmup to non-joined, rename the group-chat assemblers by scope+role
Two clarity cleanups on the group-chat subscription split:

1. RelayGroupCardWarmup (ex-Warmup) now skips a group the user has already joined.
   Joined groups are kept fully warm app-wide by the always-on state + chat-tail
   subs; warmup only needs to cover groups shown as cards that those don't — above
   all NON-joined groups (discovery, a relay's channel list, member/metadata/parent
   screens). Removes the last joined-group double-fetch. (A joined group appearing
   in the discovery "My Groups" tab now draws its card activity from the recent-tail
   cache instead of a fixed newest-50 fetch.)

2. Rename the assemblers so their scope and role read at a glance, side by side:
     RelayGroupState      -> RelayGroupJoinedState     (joined · state, always-on)
     RelayGroupPreviewTail -> RelayGroupJoinedChatTail (joined · recent chat, always-on)
     RelayGroupChatTail   -> RelayGroupOpenChatTail    (open group · recent chat)
     RelayGroupChatHistory -> RelayGroupOpenChatHistory (open group · older chat)
     RelayGroupWarmup     -> RelayGroupCardWarmup      (on-screen card, non-joined)
   The family now reads Joined{State,ChatTail} · Open{ChatTail,ChatHistory} · CardWarmup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 13:57:44 +00:00
Claude 050ecaaf9f feat(nip29): split group chat into always-on state + live tail + history pager
Consolidates the six overlapping NIP-29 group-chat REQ assemblers into the
state-vs-content shape the DM and Concord chat stacks already use, reusing the
BackwardRelayPager / RelayLoadingCursors / WindowLoadTracker framework. See
amethyst/plans/2026-07-18-nip29-group-chat-subscriptions.md.

State (always-on, account-keyed), mounted at LoggedInPage:
- RelayGroupStateFilterAssembler — one #d filter per host relay carrying every
  joined group's 39000/39001/39002/39003/39005. Keeps name/roster/roles/pins
  current app-wide, so no screen re-queries metadata. Promotes (and replaces) the
  old RelayGroupMyJoinedGroups roster path from "while a groups screen is up" to
  genuinely always-on.
- RelayGroupPreviewTailFilterAssembler — one #h filter per host relay across all
  joined group ids, since=recentBoundary(), NO per-group limit (a time floor
  batches it and makes it reconnect-safe). Drives the Messages-list previews.
  Replaces the old fixed-window content path whose shared per-relay `since` gated
  a newly-joined group's backfill (the reported slow-first-load bug).

Content (per open group), mounted on the chat screen:
- RelayGroupChatTailFilterAssembler — the open group's recent chat live (covers a
  non-joined group opened by link, which the joined-only preview tail misses).
- RelayGroupChatHistoryFilterAssembler — on-demand backward pager (until+limit on
  the host relay, all authors), cursors on RelayGroupChannel.history, driven by
  the feed's viewport markers with an eager backfill-to-window on open. Gap-proof
  via RelayLoadingCursors.rewindTo, so deep scroll never misses older messages —
  and being all-authors it re-materializes my own history too.

Retires the duplicated paths:
- RelayGroupMyJoinedGroups{FilterAssembler,Subscription} deleted (state + preview
  subs cover it); unmounted from Messages panes, discovery, bottom-bar preloader.
- ChannelPublicFilterSubAssembler RelayGroupChannel branch drops the content
  window, keeps only metadata + pinned-id backfill (for a non-joined open group).
- ChannelFromUserFilterSubAssembler RelayGroupChannel branch removed (redundant).
- FilterMessagesToRelayGroup / FilterMyMessagesToRelayGroup deleted (orphaned).

Warmup (non-joined cards), OnRelay (directory), Discovery, ThreadFeed and the
always-on notifications path are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 04:19:50 +00:00
Claude af9d5a7278 docs(nip29): plan to split group chat into always-on state + paginated content
Design doc for consolidating the six overlapping NIP-29 group-chat REQ assemblers
into the same state-vs-content shape the DM and Concord chat stacks already use:
an always-on account subscription for the small replaceable state (metadata /
roster / roles / pins), and a live tail + on-demand backward history pager
(reusing BackwardRelayPager / RelayLoadingCursors / WindowLoadTracker) for the
high-volume chat content. Includes the full message-delivery coverage proof
(can't-miss-messages), the file-by-file change list, and the additive-first
rollout order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 04:19:25 +00:00
Claude 7dcf2afa0f perf(chats): make ChannelNewMessageViewModel.init idempotent across recompositions
Every channel composer screen (NIP-28 public chat, NIP-29 relay group, NIP-53
live activity, ephemeral and geohash chats) calls `init(accountViewModel)`
directly from its composable body, so it ran on the main thread on every
recomposition of that body — re-allocating UserSuggestionState,
EmojiSuggestionState and a fresh ChatFileUploadState each time. Besides the
wasted main-thread allocations, blindly re-initializing `uploadState` would
reset an in-progress upload.

Guard the setup so it only (re)runs when the account actually changes, matching
the pattern the sibling ConcordNewMessageViewModel already uses. Repeated calls
with the same account are now cheap no-ops, so leaving the call in the composable
body stays correct while dropping the per-recomposition work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 01:25:52 +00:00
Claude 9d8e07fbee perf(nip29): prefetch a joined group's history with a per-group since
The Messages-list warmup (RelayGroupMyJoinedGroupsSubAssembler) is keyed by
account, so its `since` is one per-relay EOSE map shared across every joined
group on that relay — not per-group. Feeding that shared `since` into the
bounded per-group content backfill gated any group joined (or first surfaced)
after that relay's `since` advanced — or after a live message on a sibling group
pushed it forward: the group only ever fetched events newer than that timestamp,
so its history never landed in the Channel's (strong-ref) notes cache. The
Messages row showed the newest message while opening the group waited on a full
`limit=200` relay round-trip — "the last message is there but the group loads
slowly even though it should be cached."

Dropping `since` entirely would be just as wrong: the pool re-sends every REQ on
each reconnect (FiltersChanged treats a brand-new connection's empty prior state
as a change), and reconnects happen constantly, so a since-less filter would
replay the whole newest-50 page for every group on every reconnect.

Instead, gate the shared `since` per group on whether we already hold a full page
(>= LIMIT) of that group's preview content in the notes cache:
  - < LIMIT cached  -> cold / newly joined / thinly scattered: fetch the full
                       page with no `since`; once it lands it flips to incremental
                       on its own.
  - >= LIMIT cached -> already backfilled: apply the shared `since` so reconnects
                       fetch only the tail (the group already has its history, so
                       sharing the relay `since` only bounds incremental top-ups).
A group with genuinely fewer than LIMIT total events re-pulls its (sub-page,
cheap) content on reconnect — an acceptable cost for guaranteeing the backfill.

Roster stays on the shared per-relay `since`: those are a few small replaceable
events per group, so a reconnect just re-confirms them rather than replaying chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDK63toGbE7DQxKxrQnhMU
2026-07-18 01:25:28 +00:00
Vitor PamplonaandGitHub cd5060e5dc Merge pull request #3626 from vitorpamplona/claude/graperank-algorithm-improvements-oco5ue
Add follower crawl and trusted-follower metrics to GrapeRank
2026-07-17 19:29:49 -04:00
Vitor PamplonaandGitHub bdbf12f8be Merge pull request #3628 from vitorpamplona/claude/mediaservers-settings-ui-design-gbq9m7
Redesign media servers UI with drag-reorder, health probes, and cards
2026-07-17 19:28:01 -04:00
Claude 699c2dc7e5 fix(graperank): import kotlinx.coroutines.IO for Kotlin/Native compile
FollowerCrawler is in commonMain but used `Dispatchers.IO` without importing
the multiplatform `kotlinx.coroutines.IO` extension. On JVM `Dispatchers.IO`
resolves to the JVM member (compiled fine locally), but on Kotlin/Native that
member is internal, so `:quartz:compileKotlinIosSimulatorArm64` failed with
"Cannot access 'val IO': it is internal". Add the explicit import — the same
idiom the sibling GrapeRankCrawler already uses across all targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
2026-07-17 23:26:46 +00:00
Claude d2e18cb18d fix(media-servers): trim the gap above the first section header
The first "Upload priority" header stacked FeedPadding's top inset, an
extra 8dp, and SectionLabel's 20dp inter-section top padding — ~38dp of
dead space under the top bar. Make SectionLabel's top padding a parameter
and shrink it for the first section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
2026-07-17 23:26:34 +00:00
Claude a99fc59de8 feat(media-servers): auto-save on every change; polish cache card & add flow
Addresses review feedback on the redesign:

- Auto-save: drop the Save button and replace the cancel "X" with a
  standard back arrow (TopBarWithBackButton). Every add/remove persists
  immediately; a reorder persists once the drag gesture ends, so dragging
  no longer publishes a kind-10063 event on every intermediate swap.
- On-device cache card now uses the app's filled surfaceContainer card
  style (no outline), matching the main Settings screens.
- Drop the redundant intro sentence at the top of the screen.
- Rework the "Add a server" block: recommended servers come first as
  one-tap chips, followed by the "or paste a server address" field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
2026-07-17 23:21:02 +00:00
Vitor PamplonaandGitHub 0ac31a7919 Merge pull request #3627 from vitorpamplona/claude/commons-crowdin-ci-acdrjg
chore: include commons Compose resources in Crowdin sync
2026-07-17 19:20:24 -04:00
Claude 7f99cbc3d6 fix(ci): include commons translations in the Crowdin PR
The Crowdin action now downloads the commons module's Compose-resource
translations too, but the create-pull-request step's add-paths still only
covered amethyst/ and docs/. New untracked commons files created by the
Crowdin action (run as root in its container) were therefore left out of
the commit and shelved by git stash --include-untracked, which failed with
'Permission denied' when trying to remove the root-owned files.

Add commons/src/commonMain/composeResources/**/strings.xml to add-paths so
those translations are committed into the PR instead of being stashed.
2026-07-17 23:14:36 +00:00
Vitor PamplonaandGitHub 451e27e38d Merge pull request #3625 from vitorpamplona/claude/event-renderers-commons-7pttym
refactor(commons): extract note-type UI rendering to commons
2026-07-17 19:04:41 -04:00
Claude 4d199ab28a fix(media-servers): probe /upload not root; polish rows into cards
Health probe was hitting the bare server root with HEAD. CDN-fronted hosts
like cdn.satellite.earth never answer `/` (verified: HEAD/GET `/` time out,
while HEAD `/upload` returns 404 in ~0.5s), so a fast, working server was
wrongly shown Offline. Probe the BUD-01 `/upload` endpoint instead — the
path that actually matters for an upload target.

Visual polish:
- Server rows are now outlined cards; the primary target (#1) gets an accent
  border, a faint accent tint, and a "Primary" badge.
- Rank and monogram merge into one avatar (rank as a small corner badge),
  removing the separate rank tile and decluttering the row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
2026-07-17 23:03:05 +00:00
Claude 33f4c648e9 fix(graperank): don't create the operator master for a read-only follower crawl
`amy graperank followers <observer>` (no --relay) assembles the relay universe
via allKnownRelays, which read the reachability cache through ctx.reachability —
and that lazily derives the monitor key from the operator master, forcing a
passphrase prompt (or failing on a fresh machine) purely to READ kind:30166
records. The follower crawl signs nothing, so this defeats its anonymous path.

Read the reachability snapshot with a throwaway signer instead (snapshot() only
reads; the signer is used solely for writes) — the same trick `graperank status`
already uses to stay side-effect-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
2026-07-17 22:57:25 +00:00
Claude 0feb0072a1 feat(media-servers): redesign whole screen as one canvas, drop tabs
Replaces the Servers/Local-cache segmented tabs with a single cohesive
scroll, and redesigns every zone of the screen:

- Upload priority: draggable, ranked server rows now carry a colored
  monogram (identity) and a "Primary" badge on #1, alongside the health dot.
- Add a server: the old inline URL field + a second 10-row recommended
  list are replaced by one inline add area — the URL field plus the
  recommended servers as a horizontal strip of add-chips that read as
  "done" once added (matched by host so URL normalization can't hide it).
- On-device cache: the old bare switch rows become a self-contained card
  with a storage icon, a detection-status dot, and the profile-pics-only
  sub-toggle nested under it.

Removes the segmented tabs and their now-dead strings. Same staged
Save/Cancel model, kind-10063 publish, drag utility, and health probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
2026-07-17 22:53:13 +00:00
Claude ee217a42dc perf(commons): remember derived values in the moved note cards
Audit follow-up. These renderers sit in scrolling feeds; each recomputed
event parses / string builds directly in the composable body instead of
caching them against the (immutable) event — the CLAUDE.md rule-#4 pattern.
The cards are already skippable (immutable event params + strong skipping),
so this is work redone per composition (each item scrolling into view), not
per frame; still worth removing. These patterns pre-existed in the Android
originals and were carried over faithfully — this cleans them up now that the
code is shared.

- RelayDiscoveryCard: the heaviest — 6 `joinToString` + a `.sorted()` ran in
  the body. Now each display string (network / relay-type / requirements /
  supported-NIPs / accepted-kinds / geohashes, the requirements lock flag, and
  the relay-URL displayUrl()) is `remember`ed off its parsed list; the row
  visibility checks still key off the original lists so behavior is identical.
- CalendarRsvpCard: `status` / `calendarEventAddress` / `freebusy` parses now
  `remember(event)` instead of re-scanning tags every composition.
- CalendarCollectionCard: `title()` and `calendarEventAddresses().size` (which
  allocated a whole List just to read size) now `remember(event)`.
- PodcastValueSplits: the `recipients.filter{}` + `totalSplit()` now
  `remember(value)`.

Behavior is unchanged (same keys, conditions, and outputs). Verified:
:commons:compileKotlinJvm + :amethyst:compileFdroidDebugKotlin pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:09 +00:00
Claude c4957ee8c0 fix(commons): base ColorScheme.isLight on background, not primary luminance
commons ColorScheme.isLight tested `primary.luminance() < 0.5f`. With the
default purple accent the primary is a deep purple in the light theme (lum
0.09) AND a light purple in the dark theme (lum 0.35) — both < 0.5 — so it
reported "light" in BOTH modes. The Android app decides the same thing from
the background (`background != Color.Black`); `background.luminance() > 0.5f`
is the multiplatform-safe equivalent (light bg ≈ 0.98, dark bg = 0.0).

Surfaced while reviewing the note-ui extraction: the new commons theme helpers
that branch on isLight — subtleBorder / replyModifier (card hairline borders)
and allGoodColor / warningColor (RelayDiscovery latency chips) — were rendering
their light-theme variant in dark mode, a regression vs the Android originals
which use the app's background-based isLight. This restores parity.

Also corrects two pre-existing consumers that had the same latent bug and now
behave correctly in dark mode (worth a dark-mode glance in review):
- UserAvatar → CachedRobohash light/dark variant selection;
- ChatTheme.chatBubbleBackground alpha.

Verified: :commons:compileKotlinJvm passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:09 +00:00
Claude c18256a175 docs(plan): record note-ui-commons extraction progress + findings
Amends commons/plans/2026-07-16-note-ui-commons-extraction.md with a §0.1
"Progress & findings" section after landing 13 event kinds:

- a batch table (what moved, which seam mechanic each proved) and the shared
  commons theme now in place;
- Finding A: commons i18n was an unscoped prerequisite — wired commons into
  Crowdin so migrated strings/plurals keep every locale;
- Finding B: gate #1 (amethyst reads commons Res) is the real Tier-1 unblock,
  since most renderers share strings with a still-native screen;
- Finding C: "unused seam" (declares accountViewModel/nav but never calls
  them) is the cleanest Tier-1 signal;
- Finding D: commonMain bans Jackson (blocks MedicalData/MiniFhir);
- Finding E: platform leaves become opaque or typed @Composable slots (PS1
  bitmap icon, Roadstr map) and it works cleanly.

Also flips front-matter status to in-progress and annotates the Tier 0 / Tier
1 lists in §5 with what's done and what's left. The design (§1–§8) is
unchanged; §0.1 is the amendment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:09 +00:00
Claude 14c9f99bac refactor(commons): extract NIP-52 calendar collection + RSVP cards to commons
Twelfth batch of the note-ui-commons extraction — the calendar family, and
a fuller exercise of gate #1: strings shared with three native call sites
move to commons with no duplication.

- commons/ui/note/CalendarCollectionCard.kt: the calendar collection card
  (kind 31924) — title, description, event count.
- commons/ui/note/CalendarRsvpCard.kt: the RSVP card (kind 31925) — the
  going/maybe/not-going status, note, and target address.
  Both are pure value-in; the entries' unused accountViewModel/nav are dropped.
- Strings/plural migrated to commons: calendar_rsvp_going/maybe/not_going and
  the calendar_collection_count plural. Their native co-users now read them
  from commons Res instead of the Android res tree — CalendarRsvpRow (the
  interactive RSVP row), CalendarEventDetailScreen, and CalendarCollectionsView
  — so each key lives in exactly one place.
- amethyst keeps the thin RenderCalendarCollectionEvent / RenderCalendarRSVPEvent
  dispatcher entries.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:09 +00:00
Claude fd4d9c10f7 refactor(commons): extract RelayDiscovery card to commons
Eleventh batch of the note-ui-commons extraction, and the first to exercise
gate #1 (app-side reads of commons Res) for a string shared with a native
screen — no duplication.

- commons/ui/note/RelayDiscoveryCard.kt: the NIP-66 relay discovery/monitor
  card (relay URL, latency health chips, network/relay-type/requirements/
  NIPs/kinds/topics/geohashes). Pure value-in — takes the quartz event; the
  entry's unused accountViewModel/nav are dropped.
- commons/ui/theme: adds ColorScheme.allGoodColor / warningColor (the
  green/amber status colors), mirroring the Android values.
- Migrated the 10 relay_monitor_* / relay_discovery_* strings the card uses
  into commons. Seven of them are shared with the native RelayInformationScreen,
  which now reads them from commons Res instead — so the keys live in exactly
  one place. relay_monitor_reports (screen-only) stays app-side.
- amethyst keeps the thin RenderRelayDiscovery(Note, …) dispatcher entry.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:09 +00:00
Claude 60a226891c build(amethyst): read commons Compose resources; drop a duplicated string
Enables app-side (still-native) screens to reference commons' generated
`Res` directly, so a string shared between a commons renderer and an
Android screen no longer has to be duplicated across the two resource
trees. commons already exposes `Res` publicly (publicResClass = true); the
only missing piece was the Compose-resources runtime on amethyst's
classpath — the JetBrains compose artifacts are already there transitively
via :commons, this just adds components-resources so
`org.jetbrains.compose.resources.*` resolves.

Proof + cleanup: the V4V split editor now reads
podcast_value_split_percent from commons `Res` instead of the Android res
tree, and that key is removed from amethyst — collapsing the duplication
introduced when PodcastValueSplits moved to commons. (podcast_value_for_value
stays app-side: it feeds an Android-int ResourceToastMsg, which has no
Compose-resource form.)

This unblocks the next wave of renderer extractions whose strings are
shared with native screens (relay discovery, calendar, …) — those screens
can now switch to commons `Res` rather than forcing a duplicate.

Verified: :amethyst:compileFdroidDebugKotlin and :commons:compileKotlinJvm
both pass; touched strings.xml well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:07 +00:00
Claude 62c2d7755d refactor(commons): extract PodcastValueSplits to commons
Ninth batch of the note-ui-commons extraction, completing the podcast
display family (badge/link/soundbite atoms landed earlier).

- commons/ui/note/PodcastValueSplits.kt: the Podcasting-2.0 value-for-value
  split breakdown card (header, split hint, one row per recipient with its
  percentage). Pure value-in — takes a quartz PodcastValue.
- commons/ui/theme: adds ColorScheme.grayText (onSurface @52%); reuses the
  Size5dp added with the ActivityCard batch.
- Consumers (PodcastEpisode, PodcastMetadata) re-point to commons.

Strings: podcast_value_zap_split_hint is renderer-only and moves fully to
commons. podcast_value_for_value and podcast_value_split_percent are also
used by the native V4V split editor — for_value via an Android-int
ResourceToastMsg toast that has no Compose-resource equivalent, and the
editor can't reference commons' generated Res until amethyst gains the
compose-resources dependency. So those two keys are intentionally duplicated
(kept in amethyst for the editor, copied to commons for the shared card).
The duplication collapses once amethyst can read commons Res or the editor
itself moves; both sources stay in Crowdin meanwhile.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:50:06 +00:00
Claude fb471a2e6b refactor(commons): extract Roadstr road-event card to commons
Eighth batch of the note-ui-commons extraction. The two Roadstr renderers
(report kind 1315, confirmation kind 1316) are self-contained except for the
map hero, which is a platform tile view.

- commons/ui/note/RoadEventCard.kt: RoadEventReportCard(event, map) and
  RoadEventConfirmationCard(event, map). Commons owns all the logic — the
  category emoji/color/label palette, the geohash→point resolution, the
  freshness-based pin opacity, the floating category pill, and the layout.
  The map arrives as a typed RoadEventMap slot
  (lat, lon, pinColor, pinEmoji, pinAlpha) so the native LocationPreviewMap
  stays in the app. Labels resolve via commons Res.
- amethyst keeps thin RenderRoadEventReport(Note) /
  RenderRoadEventConfirmation(Note) entries that decode the event and pass
  LocationPreviewMap as the slot.
- Migrated all 18 road_event_* strings with their locale translations into
  commons/composeResources; deleted the amethyst copies.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:49:36 +00:00
Claude 05f7da6814 refactor(commons): extract activity-card building blocks to commons
Seventh batch of the note-ui-commons extraction. The activity-card slot
composables — the gradient frame, kind badge, header row, amount row, and
pill shared by the reaction / zap / nutzap renderings — are pure
value/slot-in with no strings, so they move as-is.

- commons/ui/note/ActivityCard.kt: ActivityCardFrame, ActivityBadge,
  ActivityPill, ActivityHeaderRow, ActivityAmountRow, and the LikeTint color.
- commons/ui/theme/Sizes.kt: shared Size16Modifier (and Size5dp for the next
  batch), mirroring the Android ui/theme values.
- Consumers (Reaction, ZapEvent, Nutzap) re-point their imports to commons.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:49:35 +00:00
Claude 71d15bf916 refactor(commons): extract podcast badge/link/soundbite atoms to commons
Sixth batch of the note-ui-commons extraction. These are the shared
Podcasting-2.0 display atoms the show/episode renderers compose from; they
already take plain values + onClick/onPlayFrom lambdas, so they move as-is.

- commons/ui/note/PodcastChips.kt: PodcastBadge + PodcastLinkChip (now
  public), pure value/lambda-in pills.
- commons/ui/note/PodcastSoundbites.kt: the soundbite "jump to the good
  part" chip row; the play label resolves via commons Res, playback stays
  controller-agnostic through the onPlayFrom callback.
- Migrated podcast_play_soundbite with its locale translations into
  commons/composeResources; deleted the amethyst copy.
- Consumers (PodcastEpisode, PodcastMetadata, PodcastEpisodeAudioPlayer,
  PodcastChaptersView) re-point their imports to commons.

PodcastValueSplits is intentionally left for a follow-up: two of its strings
are shared with the native V4V split editor, so it needs the editor
re-pointed in the same change.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:49:35 +00:00
Claude 3b96dc2f82 refactor(commons): extract PS1 memory-card save card to commons
Fifth batch of the note-ui-commons extraction, and the first to use the
platform-specific @Composable slot pattern the plan calls for (§3e/§4).

The PS1 save card (kind 38192) is pure value-in except for its animated
16×16 icon, which is decoded from raw memory-card pixels via Android Bitmap
APIs (createBitmap/setPixels/asImageBitmap) that can't move to commonMain.

- commons/ui/note/Ps1SaveCard.kt: the card takes decoded primitives (title,
  filename, region, block number, blank flag, hex preview) plus an
  `icon: (@Composable () -> Unit)?` slot; a null icon falls back to the
  floppy-disk emoji prefix. Labels resolve via commons Res.
- amethyst keeps the thin RenderPs1Save(Note) entry, which decodes the event
  and supplies the native Ps1SaveIconImage (the bitmap/frame-clock animation)
  as the icon slot.
- Migrated ps1_save_title/ps1_save_block/ps1_save_empty_slot with all locale
  translations into commons/composeResources; deleted the amethyst copies.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:49:34 +00:00
Claude a9ff6f5fbb refactor(commons): extract Birdstar Birdex/detection cards to commons
Fourth batch of the note-ui-commons extraction. The two Birdstar cards
(Birdex kind 12473, bird detection kind 2473) are pure value-in renderers:
they only read decoded tag values and open one Wikidata link, with no
AccountViewModel/INav.

- commons/ui/note/BirdexCard.kt: BirdexCard(BirdexEvent) and
  BirdDetectionCard(BirdDetectionEvent). The scientific-name link reuses the
  commons ClickableUrl atom; labels resolve via commons Res.
- amethyst keeps thin RenderBirdex(Note) / RenderBirdDetection(Note) entries
  that decode the event and call the shared cards.
- Migrated bird_detection_title (string) plus birdex_species_count and
  birdex_species_preview_more (plurals) with all locale translations from
  amethyst/res into commons/composeResources; deleted the amethyst copies.

Verified: :commons:compileKotlinJvm and :amethyst:compileFdroidDebugKotlin
both pass; every touched strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:48:33 +00:00
Claude b2c78d6b76 refactor(commons): extract GitDiffView renderer to commons
Third batch of the note-ui-commons extraction. GitDiffView is a pure
presentation composable — it takes a quartz ParsedPatch + a Modifier, with
no AccountViewModel/INav/Note — and its syntax-highlighting helper
(CodeHighlighter) already lives in commons, so it moves wholesale.

- commons/ui/note/GitDiffView.kt: the GitHub-style file-by-file diff view
  (stat summary, per-file collapsible cards, +/- line coloring, intraline
  emphasis, syntax highlighting). Labels resolve via commons Res.
- Migrated git_diff_binary (string) and git_diff_files_changed (plural,
  all quantity forms across 11 locales) from amethyst/res into
  commons/composeResources; deleted the amethyst copies. This exercises the
  now-wired commons Crowdin pipeline for a plural resource.
- Callers (Git.kt, GitPullRequestChanges.kt, GitCommitLog.kt) re-point the
  import to commons; the call sites are unchanged.

Verified: :commons:compileKotlinJvm (plural accessor generation) and
:amethyst:compileFdroidDebugKotlin both pass; every touched strings.xml is
well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:48:31 +00:00
Claude a839ef226a refactor(commons): extract GitStatusPill to commons; wire commons i18n
Second batch of the note-ui-commons extraction, and the load-bearing
setup for every string-bearing renderer that follows.

Commons had no translation pipeline — crowdin.yml synced only the Android
app's strings.xml — so moving an already-translated string to commons
Res.string would drop its translations and remove it from future syncs.
This commit fixes that first:

- crowdin.yml: add commons/composeResources as a second Android source
  with the same language mapping, so shared keys keep flowing through
  Crowdin.
- Migrate the four git_status_* pill labels (open/merged/closed/draft)
  and all 56 existing locale translations from amethyst/res into
  commons/composeResources, deleting the amethyst copies. No language
  regresses.

Then the renderer split (NIP-34 git status pill):

- commons/ui/note/GitStatusPill.kt: the StatusKind enum + the pure
  GitStatusPill(kind, modifier) presentation composable, resolving labels
  via commons Res.string. Shared by Android and Desktop.
- amethyst keeps the thin GitStatusPill(targetIdHex, …) entry that reads
  the account-bound GitStatusIndex and delegates to the commons pill.
- Callers (GitItemListRow, Git.kt) re-point StatusKind to commons.

Verified: :commons:compileKotlinJvm (accessor generation + all 56 locale
qualifiers accepted) and :amethyst:compileFdroidDebugKotlin both pass;
every modified/added strings.xml is well-formed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:47:39 +00:00
Claude 870aeb4b99 refactor(commons): extract CodeSnippet + EcashMint renderers to commons
First pilot of the note-ui-commons extraction plan
(commons/plans/2026-07-16-note-ui-commons-extraction.md, Tier 0). These
two event-kind renderers take plain quartz events with no AccountViewModel,
INav, or R.string, so they move to commons/ui/note/ almost verbatim:

- EcashMintCard: RenderCashuMint / RenderFedimint / RenderMintRecommendation
  move wholesale (already past the seam — the NoteCompose dispatcher hands
  them a decoded event). NoteCompose + ThreadFeedView imports re-pointed.
- CodeSnippetCard: the feed preview and thread header layouts move to
  commons; the Android app keeps a thin RenderCodeSnippetEvent(Note) entry
  that decodes the Note and calls the shared card.
- NoteBorders: shared QuoteBorder/SmallBorder/StdHorzSpacer/StdVertSpacer/
  subtleBorder/replyModifier theme primitives in commons so the migrated
  cards share one visual vocabulary. Mirrors the Android ui/theme values;
  the Android copies are deleted once every renderer that uses them moves.

Desktop now renders the identical cards, so the two front ends can't drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
2026-07-17 22:47:37 +00:00
Claude 331dca79cd feat(graperank): run crawl/score without a personal account
crawl and score used Context.open, which requires an account solely to default
the observer to the logged-in user — but neither uses account keys: crawl never
signs, and score signs cards with the machine-level operator key (~/.amy/operator/,
independent of any account). Switch both to Context.openOrAnonymous and require
an explicit OBSERVER when running anonymously, matching `graperank followers`.

A machine acting as a GrapeRank provider for arbitrary observers no longer needs
a personal account (the operator key still needs its SecretStore passphrase, as
before — that's the reachability monitor + card signer, a real secret).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
2026-07-17 22:41:26 +00:00
Vitor PamplonaandGitHub 90acafe389 Merge pull request #3624 from vitorpamplona/claude/relay-icons-reaction-gallery-x4uzmk
Move relay badges from author column to reaction gallery
2026-07-17 18:31:10 -04:00
Vitor PamplonaandGitHub 23597ab883 Merge pull request #3623 from vitorpamplona/claude/messages-settings-toggles-s1qo9e
Add per-chat-type load toggles to Messages settings
2026-07-17 18:30:57 -04:00
Claude ed9e02ac7f fix: reorder relay line last, align its icon, show expand button for relays
Address gallery layout feedback for the "accepted by relays" line:
- Move the relay line to the bottom of the reaction detail gallery.
- Use NotificationIconModifier for the Dns icon box so it lines up
  horizontally with the zap/like/nutzap category icons (which carry the
  same 5dp end padding).
- Show the reaction-gallery expand button whenever the note has relay
  URLs, not only when it has reactions/zaps/boosts, so the always-present
  relay line is reachable. Gate it with a lightweight boolean relay
  observer (no list allocation, no sampling) since it runs per feed note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVPYTTnMbxwi5hUAagsKVc
2026-07-17 22:20:49 +00:00
Vitor PamplonaandGitHub 3316dfcd7a Merge pull request #3622 from vitorpamplona/claude/concord-epoch-backfill
feat(concord): backfill channel history across Refounding epochs
2026-07-17 18:04:16 -04:00
Vitor PamplonaandGitHub 880a980f82 Merge pull request #3616 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-17 17:51:09 -04:00
vitorpamplonaandgithub-actions[bot] cbcb8d42ba chore: sync Crowdin translations and seed translator npub placeholders 2026-07-17 21:49:24 +00:00
Vitor PamplonaandGitHub 2fee4e800f Merge pull request #3620 from vitorpamplona/claude/medicaldata-jackson-kotlin-serialization-c15i39
Replace Jackson with kotlinx.serialization for FHIR parsing
2026-07-17 17:49:17 -04:00
Vitor PamplonaandClaude Opus 4.8 1d5b08a128 docs(concord): mark epoch-walking backfill steps 1-3 done
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:48:45 -04:00
Vitor PamplonaandClaude Opus 4.8 602c6a90b2 feat(concord): page channel history across epochs to the true start
The backward "load older" pager REQ'd only the current epoch's Chat Plane, so
deep scroll stopped at the last Refounding and showed "All caught up" while
older messages sat under prior-epoch planes.

Widen the history REQ authors to the union of the channel's plane pubkeys
across every held epoch (ConcordCommunitySession.channelPlaneAddressesAllEpochs).
The relay serves them interleaved by created_at, so one backward `until` sweep
walks the whole cross-Refounding timeline and `exhausted` (the "All caught up"
signal) now means every epoch is drained, not just the current one. The pager
is unchanged — it only tracks until/limit per relay and forwards createdAt; the
prior-epoch wraps decrypt on the normal ingest path (already epoch-aware).

Test: ConcordCommunitySessionTest asserts channelPlaneAddressesAllEpochs returns
current + prior planes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:48:45 -04:00
Vitor PamplonaandClaude Opus 4.8 891e6ced91 feat(concord): backfill prior-epoch channel history from held roots
A CORD-06 Refounding rotates the community_root and bumps the epoch, so each
channel's pre-refounding messages live under a different derived Chat Plane
per epoch. The client only ever subscribed to the current epoch's plane, so
older history was invisible and the feed said "All caught up" while months of
messages sat on the same relays under prior-epoch stream keys.

The account already persists each rotated-out root in
ConcordCommunityListEntry.heldRoots; this consumes them on the read side:

- ConcordActions.historicalChannelPlanes() re-derives each folded channel's
  plane at every held epoch (bounded by MAX_BACKFILL_EPOCHS = 8; 0 disables).
- ConcordCommunitySession keeps a historicalChannelKeysByAddress map (derived
  in refold, since channels are known only after a fold) and folds it into
  channelAddresses() (subscribe), streamKeys() (NIP-42 AUTH), and ingest()
  (decrypt with the matching epoch, isBoundTo per epoch). Channel ids are
  epoch-invariant, so historical messages merge into the same channel feed.
- ConcordSubscriptionPlanner.channelPlaneSubs appends the historical planes,
  so the existing filter assembler subscribes to them unchanged.

Writes / moderation / rekey stay strictly on the current epoch. Cross-validated
against amy: the app now subscribes to the exact prior-epoch plane pubkeys
`amy concord read --epoch 0` proved hold the older Soapbox #nostrhub messages.

Tests: ConcordCommunitySessionTest.ingestsPriorEpochWrapsFromAHeldRoot,
ConcordSubscriptionPlannerTest.channelSubsAlsoCoverPriorEpochPlanesForHeldRoots.

Follow-up (plan step 3): BackwardRelayPager epoch-stepping so deep "load older"
scroll crosses epochs to the true start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:48:45 -04:00
Vitor PamplonaandGitHub 60d492e2bc Merge pull request #3619 from vitorpamplona/claude/nip-84-tag-rendering-0o9owh
NIP-84: Support W3C TextQuoteSelector for highlight context
2026-07-17 17:47:18 -04:00
Claude db586e30d3 feat: render NIP-84 highlights from web highlighter clients
Web-based highlighter clients publish kind:9802 highlights with W3C Web
Annotation selectors (textquoteselector / textpositionselector /
rangeselector) instead of a NIP-84 `context` tag. These were previously
ignored, so the highlight rendered without its surrounding paragraph and
the "jump to page" link couldn't disambiguate repeated quotes.

- Parse the W3C textquoteselector into TextQuoteSelectorTag (exact/prefix/
  suffix; a "-" or empty exact is treated as a placeholder since the quote
  lives in .content).
- HighlightEvent.contextOrReconstructed() prefers an explicit `context`
  tag and otherwise rebuilds the paragraph from prefix + content + suffix,
  so the in-context bolding still works.
- Build a disambiguated Text Fragment URL (`#:~:text=prefix-,exact,-suffix`)
  from the selector's prefix/suffix so the source link scrolls to the
  correct occurrence.

The position/range selectors are left unparsed; they only matter for an
in-app live-page re-highlighter, which we don't have.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Di4UurD9SQGrpScX7uy2Kh
2026-07-17 21:40:21 +00:00
Claude 40acd69781 refactor(media-servers): cache health probes, prune stale status, save tab
Follow-up on the audit of the Media Servers redesign:

- Add a process-wide TTL cache (60s) to MediaServerHealthProbe so probe
  results survive the screen's ViewModel being recreated on each open;
  the ViewModel reuses fresh cached status instead of re-hitting the
  network, and skips launching a probe when one is already in flight.
- Prune the _health map on refresh/remove so it can't grow unbounded as
  servers are added and removed within a session.
- Persist the selected tab across configuration changes (rememberSaveable).
- Restore the screen's intro caption, reusing the previously orphaned
  set_preferred_media_servers string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
2026-07-17 21:33:26 +00:00
Claude dc157e32d2 fix(graperank): page through ALL followers, not just the first limit
FollowerCrawler set the reverse-lookup filter's `limit` to the page size, but
fetchAllPages treats a filter `limit` as the TOTAL cap across all pages and
stops paging once it's reached — so the crawl silently capped at ~500 followers
per relay (verified live: relay.damus.io returned exactly 500 for a
many-thousand-follower observer).

Leave the filter limit null by default so pagination walks the whole result set
(the same observer now returns 12,823 followers from damus alone); Config gains
`maxPerRelay` and the CLI a `--max N` flag to opt back into a bounded spot check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc3Wm4qVCrAGvSAotTUVt4
2026-07-17 21:32:16 +00:00
Claude 1daa871d95 refactor: move MiniFhir/MedicalData parsing from Jackson to Kotlin Serialization
Replace the Jackson-based FHIR resource parser with kotlinx.serialization.
The `resourceType` polymorphism is now handled by a JsonContentPolymorphicSerializer
that dispatches to the modeled types (Practitioner, Patient, Bundle,
VisionPrescription) and falls back to a new UnknownResource for anything else,
so a Bundle mixing known and unknown resources still parses.

The reader is lenient (ignoreUnknownKeys, isLenient, explicitNulls=false,
coerceInputValues) so we parse what we can and tolerate the missing or extra
fields that many FHIR implementations add.

Adds MiniFhirTest covering the vision-prescription bundle, extra/unknown field
tolerance, unknown-resourceType fallback, mixed bundles, and garbage input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUeR8YFNHvBELCaLRDTpCn
2026-07-17 21:27:31 +00:00