Commit Graph
14728 Commits
Author SHA1 Message Date
Claude 1e76705c87 Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ
# Conflicts:
#	commons/src/commonMain/composeResources/values/strings.xml
2026-06-08 18:15:56 +00:00
Vitor PamplonaandGitHub 8c6b94fc6f Merge pull request #3147 from davotoula/audio-visualiser
Audio visualiser for audio notes
2026-06-08 12:33:16 -04:00
David KasparandGitHub 6bf9f0e929 Merge pull request #3146 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-08 18:02:31 +02:00
davotoula 96237535ec Delay live visualizer to compensate output latency
- fix(audio): delay live visualizer to compensate output latency
- feat(audio): route-aware visualizer delay (wired vs Bluetooth)
- docs(audio): explain the hardcoded visualizer delay; note auto-detect was rejected
2026-06-08 17:46:58 +02:00
davotoula 0e3c597b96 Final code review
- RadialRenderer: guard minDim <= 0 so a transient 0-size canvas never builds Brush.radialGradient(radius = 0f), which throws.
- AuroraRenderer: clamp y with coerceIn(half, maxOf(half, h - half))
- PcmTapRegistry: never evict a flow that a composable is still collecting (subscriptionCount > 0)
- Aurora/Waves: build the palette-only gradient brushes once via remember(palette)
- audioVisualizerHeight: clamp the fallback strip to min(fallback, maxHeight)
- normalizeToPeakInPlace gains a fromIndex param
- Remove unused silentSpectrum()
2026-06-08 17:46:58 +02:00
davotoula 40642fbcad Manual visual tuning
fix(audio): make Waves & Aurora spectrum-shaped and bounded to the canvas
2026-06-08 17:46:58 +02:00
davotoula da9ac36957 Code review and manual testing fixes
refactor(audio): narrow sink visibility, exhaustive style when, clarifying docs
fix(audio): bound tap registry, uniform blurhash scrim, robust fullscreen sizing, settings preview
refactor(audio): dealias log bins, static renderer, opt-in clock, path reuse, hue util
fix(audio): persist visualizer choice via NIP-78 (publish on change)
perf(audio): reuse FFT/window buffers to cut audio-thread allocations
feat(audio): fill height in fullscreen, fixed strip in feed; punchier aurora
2026-06-08 17:46:58 +02:00
davotoula be6acabdee Add audio-visualizer settings
feat(audio): wire audio-visualizer settings into navigation and menu
feat(audio): add audio-visualizer settings screen with live previews
feat(audio): add audio-visualizer settings strings
fix(audio): move visualizer setting to Account section; render at feed size
feat(audio): add Classic (default) and Static visualiser styles
2026-06-08 17:46:58 +02:00
davotoula 032e1bf7fb Add audio visualisers
feat(audio): show selected live visualiser for audio notes
feat(audio): add change/read accessors for audio-visualizer preference
feat(audio): expose synced audio-visualizer preference flow
feat(audio): add media prefs to synced-settings internal model
fix(audio): thread-safe tap registry, reset spectrum on track reuse
feat(audio): tap decoded PCM via TeeAudioProcessor in pooled players
test(audio): unit-test PCM sink with synthetic sine waves
feat(audio): add PCM-tap registry and FFT audio-buffer sink
fix(audio): continuous viz clock, safe peak-normalize, OFF layout, palette guards
feat(audio): add AudioVisualizer dispatcher composable
feat(audio): add renderer interface, canvas scaffold, registry, and all five styles
feat(audio): add deterministic synthetic spectrum for previews
feat(audio): add VisualizerStyle enum and palette
refactor(audio): drop Visualizer FFT helper, add peak normalization
feat(audio): add Hann windowing + PCM-to-float conversion
feat(audio): add pure-Kotlin radix-2 FFT for the visualiser
2026-06-08 17:46:58 +02:00
Crowdin Bot 56710c63bd New Crowdin translations by GitHub Action 2026-06-08 14:33:36 +00:00
Vitor PamplonaandGitHub 46e29ad3f8 Merge pull request #3145 from vitorpamplona/claude/poll-text-persistence-hFoho
Unify poll and zap poll options into single state map
2026-06-08 10:31:23 -04:00
Vitor PamplonaandGitHub aa670a4587 Merge branch 'main' into claude/poll-text-persistence-hFoho 2026-06-08 10:31:15 -04:00
Vitor PamplonaandGitHub f05500792c Merge pull request #3144 from davotoula/fix/resilient-profile-metadata
Resilient profile metadata (birthday)
2026-06-07 17:36:11 -04:00
Vitor PamplonaandGitHub 021361a72e Merge pull request #3143 from davotoula/feat/render-birdstar-birdex
Minimal first-class rendering for Birdstar "Birdex" species collections (kind 12473)
2026-06-07 17:22:08 -04:00
davotoula 0107808ef6 Code review:
- Expose a nullable descriptor
- Log the JSON element kind instead of the raw, network-sourced value.
- drop birthday happy-path tests duplicated by UpdateMetadataTest
2026-06-07 23:18:57 +02:00
davotoula 39531b85fb fix(metadata): tolerate non-spec birthday so it can't drop the profile 2026-06-07 23:05:11 +02:00
David KasparandGitHub 47f346879c Merge pull request #3142 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-07 22:56:39 +02:00
davotoula fba4b933b0 Code review:
- Make birdex_species_preview_more a <plurals> keyed on the remaining count
- Bound the species preview with maxLines=2
- Hoist the joined-names remember out of the conditional (stable slot).
- Drop the unused accountViewModel parameter
- BirdexEvent.speciesCount() derives from speciesNames().size instead of re-scanning tags
- remember() the joined species-name string so it is not rebuilt on every recomposition.
2026-06-07 22:37:05 +02:00
davotoula 98ff13b83f feat(birdstar): render Birdex species collections (kind 12473) 2026-06-07 22:18:29 +02:00
Claude e9ff46ac46 fix(dm): gate forwarded callbacks on the bound scope (single-active orchestrator)
The single-active BackwardRelayPager applies forwarded relay callbacks to
whichever scope is currently bound. Its doc already states this is "safe as long
as the caller only advances the bound scope", but a subscription for a
*just-backgrounded* scope (conversation navigation overlap, account switch, a
second pane) can still deliver a late onEvent/onEose/onClosed — which would move
the newly-bound scope's cursors instead. Now that those cursors persist on the
Chatroom/ChatroomList model, that corruption would stick.

Add BackwardRelayPager.isBoundTo(cursors) (cursor identity == scope identity) and
gate each manager's forwarded callbacks on it, so a stray callback from a
non-bound scope is dropped, not mis-applied. The framework's own newEose
bookkeeping still runs. No-op on the happy single-scope path.
2026-06-07 14:48:40 +00:00
Claude 6aaed71eea fix(dm): widen the per-relay silence window so a slow Tor connect isn't "stalled"
PerRelayLoadTracker silenced (→ stalled) any in-flight relay after 15 s of total
cohort dead air. Over Tor, REQs queue on a not-yet-connected socket and circuits
routinely take 20–80 s to come up, so relays — including the user's primary —
were being flagged "stalled" before they ever connected (visible in the Messages
trace: vitor's history REQ went out at +0 s, was silenced at +15 s, and only
actually hit the wire at +77 s). Bump the window to 60 s. lastActivityMs is
global, so any relay delivering keeps it fresh for the whole cohort — this only
fires on total dead air, and a genuinely dead relay still settles via CLOSED /
cannot-connect, not this watchdog.
2026-06-07 14:48:38 +00:00
Crowdin Bot cdb76e448c New Crowdin translations by GitHub Action 2026-06-07 12:21:45 +00:00
Vitor PamplonaandGitHub 856ff3293b Merge pull request #3141 from nrobi144/fix/nip46-bunker-double-resume
fix(quartz): NIP-46 bunker double-resume + retry id-reuse races
2026-06-07 08:19:58 -04:00
nrobi144 8b9875d9cc fix(quartz): NIP-46 bunker double-resume + retry id-reuse races
Two correctness bugs in `RemoteSignerManager` (NIP-46) and its NIP-55
sibling `IntentRequestManager`:

1. **Double-resume crash** — `awaitingRequests.get(id)?.resume(value)`
   was non-atomic. Multi-relay delivery, bunker echo/retry, and
   late-after-timeout responses could call `resume` twice for the same
   continuation, throwing `IllegalStateException: Already resumed` on a
   `Dispatchers.Default` worker.
2. **Retry id-reuse → wrong data** (NIP-46 only) —
   `launchWaitAndParse` built the request and event once, then re-used
   the same `request.id` across retry attempts. A late response from
   attempt N could resume attempt N+1's continuation with stale data.

Replace the cached-`Continuation` map with the in-house Channel-per-request
correlation pattern already used in `quartz/.../accessories/NostrClientPublishExt.kt`
(`LargeCache<id, Channel<Response>(capacity=1)>` + atomic `remove` +
`trySend` + `withTimeoutOrNull { receive() }`). Each retry attempt now
builds a fresh request with a new id; the builder is still called only
once. `finally`-block cleanup removes the cache entry on every path,
incidentally fixing a slow leak on the success path.

Adds three regression tests:
- duplicate responses → no crash + single resume (fails on \`main\`
  with \`IllegalStateException\`)
- late response after timeout → silently discarded
- late attempt-1 response does not corrupt attempt-2 result (fails on
  \`main\`: the two attempts share an id)

Design + review notes: \`quartz/plans/2026-06-03-fix-nip46-bunker-double-resume-plan.md\`
2026-06-07 14:36:29 +03:00
Claude fd8dd80172 fix(dm): show parked relays on the paused history card + make the sync marker tappable
Two reported chatroom-screen issues.

Bug 1 — the NIP-04 card's `⋯` paused state showed a bare protocol tag with no
relay count, even though tapping it listed 5 relays. historySubtitle only
counted relays that were *in-flight* (relayCount) or *stalled*; a relay that
returned a page and parked (the paused state) is neither, so it fell through to
the bare tag. The card now derives a "reaching" count from relayProgress (not
done && not stalled) — covering both fetching and parked relays — so the
subtitle reads "N relays · back to <date>", matching the popup.

Bug 2 — the in-stream "Relay sync: ✓ 5" divider was a non-interactive dead end
and didn't say which protocol it meant (it mixes NIP-17 + NIP-04). Give each
RelayReachCursor a protocol tag, make RelayReachMarkers tap-through (optional
onShowDetail callback), and add RelayReachDetailDialog listing the relays at
that point in the stream with protocol · state glyph · reach-back date. The
conversation view hoists the dialog state and wires the tap; the marker stays a
passive divider wherever onShowDetail isn't supplied (rooms list unchanged).

Compiles: commons (JVM) + amethyst. iOS not buildable in this sandbox (toolchain
download blocked) but avoids the destructuring-in-composable pattern the file
guards against.
2026-06-06 23:00:03 +00:00
Claude 27d03b4a54 fix: share poll option text between zap and non-zap polls
On the short new note screen, polls and zap polls kept their option text in
two separate maps (`pollOptions` vs `zapPollOptions`). Switching the poll type
only toggled the `wantsPoll`/`wantsZapPoll` flags, so the text typed for one
type was hidden and never reused for the other.

Make both poll types read and write the same `pollOptions` text fields, so
switching between zap and non-zap polls preserves whatever the user already
typed. The zap-specific settings (deadline, vote value range, consensus
threshold) remain independent.
2026-06-06 22:46:36 +00:00
David KasparandGitHub 09a166c7c0 Merge pull request #3140 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-06 16:20:18 +02:00
Crowdin Bot 21f5acec51 New Crowdin translations by GitHub Action 2026-06-06 14:19:43 +00:00
David KasparandGitHub f7945d7862 Merge pull request #3139 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-06 16:18:23 +02:00
davotoula a4f883eaa8 fix(scheduling): route always-on prompt to Notification Settings 2026-06-06 16:14:09 +02:00
Crowdin Bot 68d2d68edf New Crowdin translations by GitHub Action 2026-06-06 13:15:23 +00:00
Vitor PamplonaandGitHub e2b1b03d84 Merge pull request #3137 from davotoula/feat/agora-fundraiser-kind-33863
Render Agora fundraiser campaigns (kind 33863)
2026-06-06 09:13:43 -04:00
Vitor PamplonaandGitHub 8379134179 Merge pull request #3138 from nrobi144/fix/desktop-feed-reply-context
feat(desktop): reply context in feeds + profile Replies tab
2026-06-06 09:13:19 -04:00
nrobi144andClaude Opus 4.7 3a21abb0f7 fix(desktop): load parent-author metadata and make parent embed clickable
Two follow-ups to the reply-context PR.

1) Parent-author metadata wasn't reaching the embed / "Replying to @X"
   label, so they rendered the truncated hex indefinitely.
   - FeedScreen.missingNoteIds: also fetch the immediate parent EVENT
     for visible replies (was only repost originals + bech32 quotes).
   - FeedScreen.missingAuthorPubkeys: also include the parent AUTHOR
     hex, extracted DIRECTLY from each reply's tags
     (CommentEvent.replyAuthor() for NIP-22; taggedUsers().lastOrNull()
     for NIP-10) so the kind 0 request fires even before the parent
     event itself arrives in cache.
   - NoteCard.QuotedNoteEmbed + FeedScreen.rememberReplyContext:
     produceState observation of the parent author's
     metadata().flow so the embed and label recompose to display name
     + avatar once kind 0 lands.

2) Embedded parent appeared clickable but did nothing — the outer
   NoteCard's OutlinedCard onClick was catching the click and
   re-navigating to the reply's own thread (the current view). Make
   the wrapping Box itself clickable, route it to the parent thread,
   and drop the inner OutlinedCard's onClick so there's a single
   explicit click surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:11 +03:00
nrobi144andClaude Opus 4.7 0c681ecd9f fix(desktop): restore inter-word spaces in rich text with mentions/hashtags
RichTextParser splits each paragraph on ' ' so every segment is one
space-delimited token; the source space lives BETWEEN segments, not
within them. When a paragraph contains only RegularTextSegments the
parser collapses them back to one segment rejoined with " ". When the
paragraph also contains a mention/hashtag/link the segments stay split
and DesktopRichTextViewer rendered them in a FlowRow with no horizontal
gap — every word glued together.

Set the FlowRow's horizontalArrangement to Arrangement.spacedBy(4.dp)
(the same constant the file already uses for ImageGalleryParagraph),
preserving the RTL alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:11 +03:00
nrobi144andClaude Opus 4.7 a23dbb2a16 fix(desktop): tighten profile Replies tab to marked replies only
The Replies tab predicate used `!note.isNewThread()`, which returns true
whenever Note.replyTo is non-empty. The cache populates replyTo from
event.tagsWithoutCitations(), and that includes unmarked positional
NIP-10 e-tags — which modern clients use for QUOTES and MENTIONS, not
replies. Posts that merely quoted another note were therefore appearing
in the Replies tab.

Tighten the signal: a reply is now either a NIP-22 CommentEvent, or a
NIP-10 TextNoteEvent carrying an explicit `reply`/`root` marker tag
(`markedReply()` / `markedRoot()`). Unmarked e-tags no longer qualify.

Adds 6 regression tests including the unmarked-e-tag false-positive
case the user reported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:11 +03:00
nrobi144andClaude Opus 4.7 a5ac5c855e feat(desktop): add Replies tab to user profile screen
Adds a dedicated "Replies" tab between Notes and Reads on the desktop
profile screen so the reply-context rendering can be eyeballed on a
specific user's profile without scroll-hunting for an organic reply.

- DesktopProfileFeedFilter gains a repliesOnly: Boolean = false ctor
  param. Default keeps Notes-tab behavior unchanged; when true, the
  predicate becomes `event is TextNoteEvent && !note.isNewThread()`
  (excludes reposts and chat-message kinds in one check).
- UserProfileScreen: second DesktopFeedViewModel for the replies feed,
  new tab at index 1, body branch mirroring the Notes Loading/Empty/
  Error/Loaded states. Reads/Gallery/Highlights indices shift by 1.

NIP-22 kind 1111 deferred — most replies today are kind 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:10 +03:00
nrobi144andClaude Opus 4.7 ad8556a82c feat(desktop): show reply context in feeds (parent embed + label)
Detect NIP-10 / NIP-22 replies in the desktop feed pipeline and render an
embedded parent card plus a "Replying to @displayName" label above the
reply body, matching Android's home-feed behavior. Extracts the shared
ReplyToLabel composable + ReplyContext data class to commons so Android
switches over to the shared version.

- commons/.../ui/note/ReplyContext.kt: data class + from(event, cache)
  detection. NIP-10 + NIP-22 unified via BaseThreadedEvent polymorphism.
- commons/.../ui/note/ReplyToLabel.kt: shared composable.
- commons/strings.xml: new "Notes & Replies" section + replying_to key.
- desktopApp NoteCard: replyContext param + render branch (bordered
  QuotedNoteEmbed + ReplyToLabel). Recursion impossible because
  QuotedNoteEmbed's inner NoteCard call doesn't pass replyContext.
- desktopApp FeedScreen: rememberReplyContext() observes parent
  metadata flow so embed/label pop in once the parent arrives via
  relay subscription. Wired into both regular and reposted-inner paths.
- amethyst ReplyInformation.kt: removed local ReplyToLabel definition.
- amethyst Text.kt: calls shared commons ReplyToLabel; resolves author
  display name at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 15:27:10 +03:00
davotoula 7aa04e773b Code review:
- read fundraiser value tags via shared helpers
2026-06-06 12:50:29 +02:00
davotoula be12abcb21 feat(agora): render Agora fundraiser campaigns (kind 33863)
Agora (a crowdfunding client on the Ditto stack) publishes fundraising
campaigns as kind 33863 — an app-specific addressable kind with no NIP.
Amethyst had no parser or renderer, so it hit the "Event Not Supported"
path and was dropped; reposts of one rendered as a permanently blank card.

Add first-class support, modelled on NIP-99 Classifieds (title/image/body)
plus NIP-75 zap goals (goal/deadline/progress).
2026-06-06 12:50:02 +02:00
Vitor PamplonaandClaude Opus 4.8 0f915d6d68 docs(dm): reflect the quartz→commons split in the DM design doc
The design doc still described the pre-keyless engine ("BackwardRelayPager<K>...
owns the cursors... in quartz"). Update it to current reality:
- the engine paragraph: keyless single-active orchestrator that does NOT hold
  the cursors (RelayLoadingCursors live on the Chatroom / ChatroomList), binds
  per scope.
- the component map: split into "Paging primitives (quartz commonMain)" —
  RelayLoadingCursors, RelayPagingProgress — and "Paging orchestrators (commons
  relayClient/paging, jvmAndroid)" — BackwardRelayPager, PerRelayLoadTracker,
  WindowLoadTracker. Fixed stale test notes (the deleted silence test;
  BackwardRelayPagerTest now in commons jvmTest; diagnostics are amethyst-side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:53:41 -04:00
Vitor PamplonaandClaude Opus 4.8 26c0ae7f69 refactor(dm): move the paging orchestrators from quartz to commons/relayClient
Per commons/ARCHITECTURE.md, quartz is protocol/NIPs/crypto/relay framing while
commons owns the relay-subscription client and StateFlow state holders. The
paging *orchestrators* are exactly that — StateFlow-backed, subscription-loading
state — so they belong in commons, not quartz:

- BackwardRelayPager, PerRelayLoadTracker, WindowLoadTracker (+ trackingListener)
  -> commons/relayClient/paging (jvmAndroid source set, same as before).
- BackwardRelayPagerTest -> commons jvmTest.

The pure protocol-paging primitives stay in quartz commonMain:
- RelayLoadingCursors (the until+limit cursor mechanics) and RelayPagingProgress.

They had no upward deps, so the move is downhill (commons -> quartz): the
orchestrators now import RelayLoadingCursors / RelayPagingProgress from quartz.
Consumers (the six DM managers/assemblers + WindowLoadTrackerIdleTest) repoint
their imports to the commons package. The quartz geode wire test keeps testing
the relay contract; its lone BackwardRelayPager KDoc link is demoted to a
backtick (no longer reachable from quartz).

No behaviour change. DM suite green (26/26); quartz + commons compile on iOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:52:58 -04:00
Vitor PamplonaandClaude Opus 4.8 1751f41a68 docs(dm): keep the rest of the quartz paging package module-neutral
Same principle as RelayLoadingCursors: BackwardRelayPager, PerRelayLoadTracker,
WindowLoadTracker and RelayPagingProgress are reusable quartz classes, so their
docs shouldn't lean on Amethyst's Chatroom / ChatroomList / feeds / loading card
/ on-screen markers / sentinels / "decrypted into rooms" / invalidateFilters().
Reworded to generic library terms ("the caller", "the bound scope", "a
demand-driven loader", "a per-relay progress display", "the owning object").

Comment-only. (The DMPagination log tag stays — it's an established log key,
not doc prose.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:22:45 -04:00
Vitor PamplonaandClaude Opus 4.8 3fde69c3a9 docs(dm): keep RelayLoadingCursors KDoc free of downstream-module concepts
It's a reusable quartz class, so it shouldn't document itself in terms of
Amethyst's Chatroom / ChatroomList / feeds / on-screen markers / sentinels /
invalidateFilters(). Reword generically: "the caller holds one instance per
scope on whatever object owns it", "a demand-driven loader", "the loaded-back-to
point", "the owner re-issues the relay's REQ". BackwardRelayPager demoted to a
backtick mention (it's jvmAndroid, so the KDoc link can't resolve from
commonMain anyway).

Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:18:23 -04:00
Vitor PamplonaandClaude Opus 4.8 d836c06772 docs(dm): tighten the RelayLoadingCursors KDoc
Same content, ~⅓ shorter: fold the per-relay/on-demand intro into the
asked-vs-delivered framing, compress the time-window rationale and the
thread-safety note, keep the two-cursor explanation and the short-page caveat.

Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:14:19 -04:00
Vitor PamplonaandClaude Opus 4.8 50676bcc64 docs(dm): fix stale KDoc links on RelayLoadingCursors
After the class moved to commonMain and was renamed, two doc nits remained:
- [BackwardRelayPager] is a jvmAndroid type, so the KDoc link can't resolve
  from commonMain — demote it to a plain mention.
- the bare [done] links pointed at the private RelayCursor.done, not a member
  of this class — repoint them to the public [isDone].
Also reword "Not internally synchronized" (it leans on the thread-safe
LargeCache + serialized per-relay callbacks) to not read as a contradiction.

Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:11:51 -04:00
Vitor PamplonaandClaude Opus 4.8 c512bd39c4 refactor(dm): rename UntilLimitPager to RelayLoadingCursors
Once the pager was split into the orchestrator (BackwardRelayPager) and the
pure per-relay cursor state that lives on the model, "UntilLimitPager" no longer
described the latter — it pages nothing, it just records how far each relay has
loaded. Rename it (and its test) to RelayLoadingCursors.

The geode wire-contract test keeps its name (UntilLimitPagingRelayTest): it
pins the relay-side `until`+`limit` paging behaviour, not the class.

Pure rename — no behaviour change. Design doc updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:07:30 -04:00
Vitor PamplonaandClaude Opus 4.8 b37f1a6e56 fix(dm): make the commons DM feed UI compile for iOS
The DM history widgets extracted into commons were never compiled for the
commons iOS target, which hid two Kotlin/Native-only breaks:

- RelayReachMarker: `toSortedMap(compareBy { it.ordinal })` + a destructured
  `(state, list)` Map.Entry inside an inline @Composable lambda don't type-infer
  on Native. Rewrite as `.entries.sortedBy { it.key.ordinal }` with explicit
  `entry.key` / `entry.value`.
- DmHistoryLoadingCard referenced RelayPagingProgress, which sat in quartz's
  jvmAndroid source set — visible to commonMain only when building JVM/Android,
  not iOS. It's a pure data class, so move it to quartz commonMain.

commons:compileKotlinIosArm64 now succeeds; JVM/Android unaffected and the DM
test suite is still green (26/26).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:57:41 -04:00
Vitor PamplonaandClaude Opus 4.8 6223617179 refactor(dm): move paging cursors onto the model, drop the keyed pager
The history pagers were keyed by account / (account, conversation) inside the
quartz engine, with all per-key state in inner hashmaps + an activeKey +
activate() machinery. But the loaders are per-account-VM and the on-screen
scope is single-active, so the key was redundant indirection.

Move the per-relay cursor *state* onto the domain object whose lifetime it
should share:
- UntilLimitPager is now keyless (per-relay cursors + a pinned floor only) and
  lives in commonMain (LargeCache<NormalizedRelayUrl, RelayCursor> — keyed only
  by relay url, which is Comparable + equals-consistent, so the sorted cache is
  safe; kotlin.concurrent.Volatile for the fields). It is stored on:
    * Chatroom.nip04History         (per conversation)
    * ChatroomList.giftWrapHistory  (account NIP-17)
    * ChatroomList.nip04History     (account rooms-list NIP-04)
  The LocalCache object graph is now the partition; cursors are dropped exactly
  when the cached messages they describe are pruned, and survive an account
  switch (no re-page on switch-back).

- BackwardRelayPager is now a keyless single-active orchestrator: it owns only
  the transient bits (in-flight tracker, stalled set, display flows) and binds
  to the active scope's cursors via bind(cursors, scope, relaysFor). Removed
  activeKey / activate() / the per-key exhausted+floor+stalled maps. Safe as
  single-active because history relays only arm while their markers are
  on-screen, so a backgrounded scope emits no callbacks.

The three assemblers resolve the scope's cursors from the account's
chatroomList and bind on newSub; the redundant `user` arg dropped from the
account-level advance/advanceAll (callers updated).

Behaviour change: switching between two conversations no longer keeps both
rooms' cursors live in one engine — each room's cursors persist on its own
Chatroom instead, so reopening a room restores its progress (strictly better).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:50:21 -04:00
Vitor PamplonaandClaude Opus 4.8 910037ad8e refactor(quartz): add TimeUtils.nowMillis, drop raw System calls in pagers
The relay-paging trackers measured elapsed wall-clock with
System.currentTimeMillis() directly. Introduce a multiplatform millisecond
clock (currentTimeMillis expect/actual across jvm/android/ios/macos/linux,
mirroring the existing currentTimeSeconds) exposed as TimeUtils.nowMillis(),
and route PerRelayLoadTracker + WindowLoadTracker through it.

TimeUtils.now() is seconds, so it can't be used for the ms-scale silence /
idle / linger timers — nowMillis() is the correct primitive. No behavior
change (same underlying clock on JVM/Android).

Note: the two trackers stay in jvmAndroid for now — @Synchronized has no
commonMain equivalent. ConcurrentHashMap is kept deliberately: LargeCache is
a sorted ConcurrentSkipListMap (Comparable, compareTo-identity keys) and the
pager keys (ConvoKey not Comparable; ChatroomKey.compareTo is hashCode-based)
don't satisfy that, so ConcurrentHashMap is the correct structure here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:48:28 -04:00