Commit Graph
615 Commits
Author SHA1 Message Date
Claude dd9ee0b5c6 fix: close audit findings — report leak, model-level rumor guards, hide share/bookmarks on private notes
Merge-readiness audit follow-ups:

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

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

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

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

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

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:51 +00:00
Claude 84497bbd47 perf(commons): index URLs by first char in fixMissingSpaces
fixMissingSpaces runs on the main thread once per rendered note. The scan
introduced in the previous commit re-tested every detected URL at every
character position (and allocated an iterator per position via firstOrNull),
i.e. O(N*U*L) for a note with U URLs — noticeable on large notes that carry
many links.

Bucket the URLs by their first character once up front and only attempt a
match at positions whose character can actually start a URL; every other
character now costs a single map lookup, keeping the pass linear in the text
length for typical content. Buckets stay longest-first so prefix URLs still
don't shadow longer ones, so the output is identical (verified against the
commons richtext JVM corpus).
2026-06-09 22:52:00 +00:00
Claude f42cc836aa fix(commons): make fixMissingSpaces work on Kotlin/Native (iOS)
RichTextParser.fixMissingSpaces used a Regex of the form
`([^ \n])?(urls)([^ \n])?` to insert spaces around URLs glued to
neighbouring text. Kotlin/Native's regex engine fails to backtrack the
optional `([^ \n])?` capture groups to zero width, so on iOS every URL was
corrupted (e.g. "https://x" became "h https://x"). That broke the
downstream segmenter, which is why :commons:iosSimulatorArm64Test reported
19 failures across the RichText/Gallery/Pdf/F4a parsers once the test binary
finally linked.

Replace the regex with a direct left-to-right scan that inserts a single
space wherever a detected URL touches a non-space/non-newline neighbour. The
scan is engine-independent, so it behaves identically on JVM and Native.
Verified equivalent to the old behaviour across the full commons richtext
JVM corpus, and the new FixMissingSpacesTest pins the cases on every target
(including iosSimulatorArm64).
2026-06-09 22:42:50 +00:00
Claude 85f9998463 revert: drop ChatroomList.changes flow and the screen's reactive use
Backs out the reactive list-refresh plumbing added in the prior commit:
removes the `changes` SharedFlow from the shared `ChatroomList` (restoring
it to its original form) and reverts Desktop `ChatroomListState` to its
original 2s poll. Room assembly is expected to move to a LocalCache.observe
approach on both platforms later, which would supersede this.

Keeps the independent Desktop list improvements (per-room unread tracking
and the mute/acceptable filter), which don't depend on the flow.

https://claude.ai/code/session_01VEukNczAYxNLBjLnqVEoZd
2026-06-09 20:57:45 +00:00
Claude 1fbefb2efe feat(desktop): bring group DM parity with Android
Makes the Desktop DM client a first-class group participant and tightens
the shared/Desktop DM paths so they match Android behavior.

- commons ChatNewMessageState: actually attach the composed NIP-14 subject
  to sent messages (the field was previously collected but dropped).
- commons ChatroomList: emit a `changes` SharedFlow on add/remove so list
  UIs can refresh reactively; dedupe the User overloads onto the room ones.
- Desktop NewDmDialog: multi-recipient selection (chips + confirm button) so
  a Desktop user can start a group, not only a 1:1.
- Desktop ChatPane/ChatroomHeader: show a group's NIP-14 subject in the
  header and add a rename dialog that broadcasts a subject change to all
  members.
- Desktop Main.kt DM ingest: route any ChatroomKeyable inner event into the
  room (covers kind 14/15 and future variants) and store self-authored
  NIP-37 drafts instead of dropping them.
- Desktop ChatroomListState: refresh reactively off ChatroomList.changes
  (with a slower safety poll), track real per-room unread via a last-seen
  mark, and hide rooms whose latest message isn't acceptable (mute/filter).

https://claude.ai/code/session_01VEukNczAYxNLBjLnqVEoZd
2026-06-09 20:16:55 +00:00
Vitor PamplonaandGitHub 3dfe418150 Merge pull request #3151 from vitorpamplona/claude/relay-message-pagination-LMqSQ
DM history: per-relay backward paging, live-tail split + prune-aware window realignment
2026-06-09 15:05:58 -04:00
Vitor PamplonaandClaude Opus 4.8 39eb25bc17 fix(commons): show "N relays" on every history marker count chip
The in-stream loading marker spelled out "N relays" only for the fully-loaded
(done) chip; active frontiers showed a bare count ("Loading: ↓ 8"). Use the
relays plural for the count fallback on every state so a count chip always reads
as a sentence ("Loading: ↓ 8 relays"). 1–2 short host names still spell out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:17:07 -04:00
davotoula 711ca547b5 fix(math): render inline equations wrapped in opening punctuation 2026-06-09 19:51:31 +02:00
Vitor Pamplona b21aecdfca Improves markers 2026-06-09 13:47:46 -04:00
Claude d6603baf1a Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-09 12:54:51 +00:00
Vitor PamplonaandGitHub 8afdfba1c2 Merge pull request #3153 from vitorpamplona/claude/equation-rendering-klegT
fix(math): collapse over-escaped backslashes in inline equations
2026-06-08 20:57:56 -04:00
Claude be06b7335b fix(math): collapse over-escaped backslashes in inline equations
Some sources (e.g. the math-academy posts) over-escape their LaTeX, so
the content carries `\\ldots` / `\\cdots` instead of `\ldots` / `\cdots`.
JLaTeXMath reads the `\\` as a forced TeX line break — splitting every
inline equation across two lines — and renders the trailing command name
as the literal letters "ldots"/"cdots", so the ellipsis symbol is lost.

Collapse doubled backslashes (`\\cmd` -> `\cmd`) before rendering inline
math; display math is left untouched since `\\` can be a genuine line
break there. No-op when the content isn't over-escaped.
2026-06-09 00:52:47 +00:00
Vitor PamplonaandGitHub dd88ae3167 Merge pull request #3152 from vitorpamplona/claude/equation-rendering-klegT
Add LaTeX math rendering for $...$ and $$...$$ equations
2026-06-08 20:36:11 -04:00
Vitor PamplonaandClaude Opus 4.8 aa6b9bc53e refactor(dm): centralize the DM history-window boundary + log prune rewinds
The "where the live tail ends and paged history begins" boundary (one week)
was copy-pasted across the gift-wrap + NIP-04 live tails, the backward history
pager floor, and the prune's recent/old split — easy to drift out of lockstep
(overlap = double-load, gap = missed messages).

- Add DmHistoryTuning: one place for liveTailSeconds + recentKeepCount, read by
  AccountGiftWrapsEoseManager, both NIP-04 SubAssemblers, BackwardRelayPager,
  and Chatroom.pruneMessagesToTheLatestOnly. Drops the duplicated
  LIVE_TAIL_SECONDS / DEFAULT_LIVE_TAIL_SECONDS constants.
- Log the prune-time window realignment under DMPagination, per scope
  ([giftwrap] / [rooms.nip04] / [convo.nip04]): relay count + newest pruned
  timestamp, so the rewind is observable in logcat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:59:04 -04:00
Vitor PamplonaandClaude Opus 4.8 96ff5316dc fix(dm): realign the per-relay download window when DMs are pruned
Memory pruning drops DM messages out of the cache but left the per-relay
paging cursors untouched, so a relay still claimed to have delivered the
dropped band (reachedUntil deep, or done) and the demand-driven loader
never re-requested it — a silent hole until app restart.

- Prune NIP-17 too: pruneMessagesToTheLatestOnly now reaps both NIP-04
  (PrivateDmEvent) and NIP-17 (WrappedEvent rumors) on one merged top-N
  cut, so a conversation is cut at a single time point (no NIP-04-without
  -NIP-17 holes). NIP-17 is the actual memory-pressure driver.
- HostStub carries the host's createdAt, so a decrypted rumor self-
  describes its outer gift-wrap time (the time the cursor pages by; the
  rumor's own time is the message time, not the wrap time).
- RelayLoadingCursors.rewindTo() pulls a relay's reached cursor up past
  the pruned band, clears done, and un-arms it (demand-driven re-fetch);
  advance() now resumes from the rewound reached point instead of the
  floor.
- LocalCache.pruneOldMessages accumulates the newest pruned created_at
  per relay (outer-wrap time for gift wraps, event time for NIP-04),
  filtered below each cursor's floor, then rewinds giftWrapHistory +
  rooms-list nip04History (account-wide) and the per-conversation
  nip04History.

The gift-wrap window is account-global, so pruning one room rewinds the
shared sweep; the interference is bounded (already-held wraps short-
circuit in consumeRegularEvent, re-fetch is demand-gated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:03:50 -04:00
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
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
Claude b4e2b5f651 refactor: cleaner math/parser integration via space-split tokens
The first cut special-cased the whole per-line loop with an
`if (mightContainMath)` branch that duplicated the word-split +
wordIdentifier logic and filtered empty words inconsistently with the
non-math path (which preserves them to keep double-spaces).

Reframe MathParser.split as a drop-in replacement for `line.split(' ')`
that returns typed Word|Math tokens, keeping math spans whole instead of
tearing them at internal spaces. For a math-free line it yields exactly
the same words (empties included), so RichTextParser collapses to a
single uniform map with an exhaustive when — no branch, no duplication.

Also fixes end-of-sentence math: a span glued to trailing punctuation
(`$x$.`) now carries that punctuation as a `trailing` field rendered
adjacent to the equation, mirroring HashTagSegment's extras, instead of
being dropped to a plain word.

https://claude.ai/code/session_01N8ZhVv9912DLGNJiErVTR4
2026-06-08 15:31:46 +00:00
Claude 1096647191 feat: render LaTeX math in notes with $...$ and $$...$$ delimiters
Posts that use the common dollar-delimiter convention (e.g. the
math-academy "Linear Independence" note) now render their formulas
as real equations instead of raw LaTeX text.

- commons: new MathParser tokenizes a line into atomic math spans
  (kept whole, since they contain spaces) interleaved with plain text,
  following the pandoc/remark-math dollar rules so currency like
  "$5 and $10" and escaped "\$" don't false-fire. New MathSegment
  carries the inner LaTeX + display flag through the rich-text pipeline.
- RichTextParser splits math out before the whitespace word-splitter
  when a line might contain math; non-math lines keep the existing path.
- amethyst: LatexEquation renders a MathSegment via JLaTeXMath, tinted
  to the current text color and sized to the font, with a raw-text
  fallback when the formula fails to parse. Wired into both the
  preview and no-preview render paths of RichTextViewer.

Scope: dollar delimiters only, regular (non-markdown) render path.

https://claude.ai/code/session_01N8ZhVv9912DLGNJiErVTR4
2026-06-08 14:47:56 +00: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
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
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 cd6537bfd5 refactor(dm): unify per-relay reach vocabulary + clarify Nip04 routing name
Two naming families described the same concept — a relay's position in its
backward history walk — and the UI name overloaded the heavily-used "window"
and REQ "limit" terms. Collapse onto one vocabulary ("Reach"):

- RelayWindowLimit          -> RelayReachCursor
- RelayWindowLimitMarkers   -> RelayReachMarkers
- RelayWindowLimitSentinels -> RelayReachSentinels

And rename the NIP-04 per-relay routing map so it reads as a map, not a list:

- Nip04DmRelays (class) / nip04DMRelays (factory) -> Nip04DmRelayRouting / nip04DmRelayRouting

Pure rename — no behavior change. Design doc updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:10:38 -04:00
Vitor PamplonaandClaude Opus 4.8 e9f2f1d7aa refactor(dm): tighten visibility of internal-only paging helpers
- BackwardRelayPager.floorFor was public but only ever called inside the
  pager (and its same-module test) — narrow to internal.
- RelayReachMarker composable was public but only rendered by
  RelayWindowLimitMarkers in the same file; the public entry points are
  RelayWindowLimitMarkers/Sentinels — make it private.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:45:04 -04:00
davotoula 7672282892 refactor: share isRenderableRepost via commons, apply on desktop
Move isRenderableRepost() (and its test) from amethyst ui/dal into
commons/ui/feeds so both platforms use one implementation, then point
desktop's isFeedNote() at it.
2026-06-05 20:47:09 +02:00
Claude 4fc24950e3 refactor(dm): cleanup + DRY/test the relay-reach gap predicate
Audit follow-up.

- Remove the 12 history-card strings (chats_history_older / all_caught_up /
  reached_start / relay_sync / subtitle{,_no_date} / waiting / relays_title /
  relay_back / incomplete{,_sub} + the chats_history_relays plural) from
  amethyst's default strings.xml: they moved to commons composeResources with
  the card and have zero remaining amethyst references. They were branch-new and
  not yet translated, so removing the default key is a clean, orphan-free delete.
  Kept chats_history_proto_* (still the card's protocolName) and chats_reply_*.

- Extract the "does this relay's reached cursor fall in this gap" check, which
  was duplicated (with off-by-one-prone >/<= boundaries) between marker placement
  (RelayWindowLimitMarkers) and the paging driver (RelayWindowLimitSentinels),
  into a single pure reachedFallsInGap(); both now call it so they can't disagree
  about which gap a cursor lives in. Add RelayReachMarkerTest pinning every
  boundary (newer strictly >, older inclusive <=, null ends).

- Fix a garbled comment in BackwardRelayPager.onSilenced.
2026-06-05 17:19:06 +00:00
Claude ace9d20690 refactor(dm): extract the history status card to commons (shared with desktop)
Phase 2 (UI), stage 2. Move DmHistoryLoadingCard + its tap-through per-relay
dialog + the historySubtitle/incompleteSubtitle helpers out of amethyst into
commons/commonMain (com.vitorpamplona.amethyst.commons.ui.feeds), so the same
"older history / all caught up / some relays didn't respond" boundary card can
back any per-relay BackwardRelayPager feed on Android and Desktop.

The card's localized date formatting (SimpleDateFormat/Locale) can't live in
commons commonMain (it targets iOS/linux/macOS, no java.*), so it's injected as
a formatReachDate: (epochSeconds) -> String lambda — the platform that renders
the card supplies its native formatter, no i18n regression. Android passes
formatHistoryReachDate (new HistoryDateFormat.kt). The dialog's per-relay reach
now uses that same month-precision formatter (was "MMM d, yyyy", now "MMM yyyy")
— a negligible cosmetic change.

Its ~11 strings move to commons composeResources, including the module's first
<plurals> (chats_history_relays). The three Android call sites (ChatroomView,
ChatroomListFeedView, LoadingReplyNote) now import the shared card/helpers and
pass the formatter; no behaviour change. The old amethyst string copies are left
in place (harmless, separate resource namespace) for a later cleanup pass.
2026-06-05 16:58:34 +00:00
Claude c3ed7e65c9 refactor(dm): extract relay-reach markers to commons (shared with desktop)
Phase 2 (UI), stage 1. Move the per-relay reach markers — RelayReachState,
RelayReach, RelayWindowLimit, RelayWindowLimitSentinels (the hoisted,
visibility-driven paging driver), RelayWindowLimitMarkers, and RelayReachMarker
— out of amethyst into commons/commonMain (com.vitorpamplona.amethyst.commons.ui.feeds)
so the Desktop chats UI (and any feed) can render the same in-stream paging
progress, not just Android.

De-Android-ified: the one Android string (chats_history_relay_sync) becomes a
CMP composeResources string in commons; the two theme constants (DividerThickness,
HalfPadding) are inlined (0.25.dp / padding(5.dp)) so the shared component carries
no app-theme dependency. Logic is otherwise byte-identical. The Android
conversation + rooms-list views now import the shared version; no behaviour change.
2026-06-05 16:50:07 +00:00
Claude 79f237ff0b Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-05 12:33:34 +00:00
davotoula 5edfe90321 feat(player): enable brightness/volume swipe in fullscreen video 2026-06-04 23:19:08 +02:00
Claude 7c95ba1ffd refactor: rename removal methods to match what they do
Three names didn't describe their behavior:

- removeFromCache → unlinkAndRemove: the method's main job is unlinking the
  note from every referrer (parents, channels, the report/card/status/poll
  indexes), not just evicting it from the map; the old name only captured
  the last step.
- removeAllChildNotes → clearChildLinks: it clears only THIS note's forward
  child collections and returns them — it does not touch the children's
  replyTo and does not remove anything from the cache. The old name sounded
  more aggressive than detachFromChildren(), which is actually the
  both-directions op.
- Note.removeOnchainZap(source) → removeOnchainZapBySource(source): too easy
  to confuse with removeOnchainZapForSource(txid, pubkey), which is the
  verification-verdict removal with anti-spoof guards. The new name matches
  its inner helper (innerRemoveOnchainZapBySource) and disambiguates the two.

Pure rename: no behavior change. Test names/comments updated to match.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 18:59:54 +00:00
Claude 23ddeba8ec fix: sever child back-references when deleting a Note (NIP-09)
deleteNote() removed the target from its parents, gatherers, and the cache
map, but never cleared its own child collections nor dropped itself from
its children's replyTo. That left a partial deletion: every child kept the
removed shell alive through replyTo (a leak), and a reply resolved later
via computeReplyTo would getOrCreateNote a *second* Note for the same id —
breaking the one-Note-per-id invariant.

Adds Note.detachFromChildren(), which clears the note's forward child
collections (via removeAllChildNotes) and severs this note from each
child's replyTo (keeping any other parents). deleteNote() now calls it
before notes.remove(), so once the note leaves the map nothing points at
the dead shell. Orphaned replies become roots, which is correct once their
parent is hard-deleted from the cache.

Adds detachFromChildren coverage to NotePruningReferenceTest.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 17:42:29 +00:00
Claude 11591826f0 fix: detach onchain-zap and nutzap sources when pruning Notes from LocalCache
LocalCache must hold a single Note per event id/address and must never
remove a Note from the cache map while another Note still strongly
references it — a dangling reference both leaks the shell and lets a
relay echo mint a second Note with the same id.

Note.onchainZaps (NIP-BC) and Note.nutzaps (NIP-61) were added after the
removal/migration routines were written and were never wired into them,
so a pruned zap-source Note leaked through its target's maps:

- removeNote() only detached reply/boost/reaction/zap/zapPayment/report/
  label, leaving the target's onchainZaps/nutzaps entry dangling when the
  source note was pruned. Now also calls removeNutzap + a new
  source-keyed removeOnchainZap (unconditional cache removal, distinct
  from the verdict-respecting removeOnchainZapForSource).
- removeAllChildNotes() cleared onchainZaps but never returned the source
  notes for removal from the cache map (asymmetric with nutzaps), so they
  lingered orphaned. Now included.
- moveAllReferencesTo() dropped labels, zapPayments, and onchainZaps when
  a replaceable's old version was superseded — silent data loss plus
  orphaned onchain sources. Now migrated and cleared like the rest.

Adds NotePruningReferenceTest covering all three paths.

https://claude.ai/code/session_01RqJPYzmjb1pR3NBeH2yY3s
2026-06-04 15:37:44 +00:00
Claude e1cdd40bb5 chore: remove superseded TimeWindowPagination, refresh DM design doc
Review prep for the DM pagination branch:

- Delete commons TimeWindowPagination + its test: the early since-based
  time-window approach, referenced only by its own test and fully
  superseded by UntilLimitPager (until+limit, gap-proof). 212 lines a
  reviewer would otherwise study for nothing.
- Bring the design doc up to the final architecture: NIP-04 per-relay
  filter scoping, per-relay independent paging (no rounds) + in-stream
  markers for the convo, the round model still used by rooms/gift-wrap,
  the WindowLoadTracker backstops and tracksReqSends gating, the
  loadingMore-starts-false fix, and the DMPagination diagnostics map.
  Marks the obsolete time-slice section as superseded.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 17:16:21 +00:00
Claude 95a38111dd Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-03 15:24:39 +00:00
Vitor PamplonaandGitHub 67d14fcc03 Merge pull request #3125 from nrobi144/fix/desktop-log-noise
fix: address root causes of 6 runtime log noise issues
2026-06-03 07:59:09 -04:00
nrobi144 37662eea45 fix(desktop): port StickToTopOnPrepend to commons and apply on home feed
Real root cause of the "stale feed on launch" perception bug: when
fresh events prepend to the desktop home feed, Compose's stable-key
diff (`items(loadedState.list, key = { it.idHex })`) preserves the
visual anchor on whatever item was already visible. The user's
previously-visible top item — once at index 0 — silently shifts to
index N as N new items are inserted above the viewport. From the
user's perspective the feed looks frozen on stale items even though
the underlying state HAS updated; switching screens unmounts
FeedScreen, recreates lazyListState at index 0, and on remount paints
from the now-current top.

Android already handles this with StickToTopOnPrepend
(amethyst/.../WatchScrollToTop.kt:133-152), but the helper lived in
the Android module and Desktop had no equivalent.

Changes:

- New commons/.../ui/feeds/StickToTopOnPrepend.kt with the same
  observer + snapshotFlow trick, ported to use plain `collectAsState`
  (replacing the Android-only `collectAsStateWithLifecycle` — the
  effect's lifecycle is already bound to composition via
  LaunchedEffect). Provides the same overloads:
    * StickToTopOnPrepend(LazyListState, firstItemKey)
    * StickToTopOnPrepend(LazyGridState, firstItemKey)
    * StickToTopOnPrepend(FeedContentState, LazyListState)
    * StickToTopOnPrepend(FeedContentState, LazyGridState)
- FeedScreen wires StickToTopOnPrepend(viewModel.feedState,
  homeFeedLazyListState) at the same scope as the hoisted lazy list
  state and the NewPostsChip.

Mutually exclusive with the NewPostsChip: the chip's visibility
predicate fires when isAtTop is false, the auto-snap fires when
isAtTop is true. Together they cover both cases:
  * user at top → events arrive → auto-snap shows them
  * user scrolled down → events arrive → chip announces them

The Android version in amethyst/.../WatchScrollToTop.kt is left in
place to avoid a wider refactor; it can be reduced to a thin delegate
in a follow-up.
2026-06-03 07:33:39 +03:00
Claude 030f2fdbfa Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt
2026-06-02 14:59:28 +00:00
nrobi144 098a74ca53 feat(desktop): add "New posts" chip with slide-from-top animation
Fixes the perceptual "stale feed on launch" bug: on cold launch the
desktop feed paints with whatever local cache had (up to 7 days old)
before relays catch up. The live updateFeedWith() path already prepends
fresh events silently, but users had no signal that fresh content
arrived unless they were already at the top of the feed (auto-snap via
StickToTopOnPrepend).

This adds a Twitter/Mastodon-style floating pill chip that slides down
from above the search header when fresh events have prepended AND the
user is scrolled below position 0. Tapping it smooth-scrolls to top
and slides the chip back up off-screen. Scrolling to top manually
also dismisses it.

Implementation:

- NewPostsChip + rememberNewPostsChipState in commons/commonMain so any
  future feed surface (incl. Android, iOS) can adopt it. Desktop wires
  it today; Android continues with the existing auto-stick + bottom-nav
  dot pattern.
- Visibility predicate is pure-function and unit-tested (5 cases).
- Predicate mirrors the inverse of StickToTopOnPrepend's "at top" check
  so the two systems are mutually exclusive — auto-snap when at top,
  chip when not.
- Chip placement: floating Alignment.TopCenter inside FeedScreen's outer
  Box, offset by the animated headerSpacerHeight (60.dp normal,
  300.dp when search is expanded) so it tracks the header card.
- Hoisted lazyListState + headerSpacerHeight one level so the chip can
  share scroll state with the LazyColumn. Existing viewport-aware
  metadata loading is unchanged (same lazyListState reference).
- Animation: slideInVertically(tween(280, FastOutSlowInEasing)) + fadeIn
  for enter; slideOutVertically(tween(220, FastOutLinearInEasing)) +
  fadeOut for exit. Initial/target offset of -fullHeight-16 guarantees
  the chip is fully off-screen above its rest position.
- Per-column scope by construction: each FeedScreen instance has its
  own chip state (deck mode shows one chip per column).
- Resets cleanly on feed mode switch (Following ↔ Global ↔ Custom)
  because rememberNewPostsChipState is keyed on FeedContentState,
  which is recreated when viewModel = remember(feedMode, activeFeedId)
  recomposes.

Plan: docs/plans/2026-06-02-feat-new-posts-chip-desktop-feed-plan.md
2026-06-02 17:16:58 +03:00
nrobi144andClaude Opus 4.7 aeb49c3cac fix(desktop): address PR review findings on feed UI refresh
5 issues from davotoula's review on PR #3124:

- #3 (protocol): inline reply emitted a minimal e/p tag set instead of
  NIP-10. Extract `commons/actions/ReplyActions.replyTo` wrapping
  `TextNoteEvent.build(replyingTo=)` (which already encodes root marker,
  reply marker, parent root-e-tag carry) + carry parent's p-tag chain via
  `notify(...)`. Replies to deep-thread notes now thread correctly in
  Damus/Primal/Coracle. Covered by `ReplyActionsTest`.

- #4 (architecture): reaction/follow/reply each inlined
  `localCache.consume + relayManager.broadcastToAll` in 5 sites with
  inconsistent ordering. Extract `desktopApp/cache/dispatch(...)` —
  canonical local-first order — and route all 5 sites through it.

- #1 (UX): related-content section scanned the cache once via
  `DisposableEffect(noteId)` and never refreshed. Switch to `produceState`
  collecting `DesktopLocalCache.eventStream.newEventBundles`; re-scan only
  when an arriving bundle contains a candidate (matching hashtag or
  author). `LargeCache.notes` is a ConcurrentSkipListMap (weakly consistent
  iterator) so the scan stays safe on the composition coroutine.

- #2 (UX): `DeckColumnContainer` re-requested focus on every
  `currentOverlay` change, stealing focus from sibling columns whenever
  any column mutated overlay state. Drop to `LaunchedEffect(Unit)` and
  wrap the column in `key(column.id)` in `DeckLayout` so the one-shot
  effect survives column reordering.

- #5 (consistency): zap totals bypassed the shared `ZapFormatter`. Wire
  `RelatedContentRow`, `CommentItem`, and `NoteActions` to
  `commons/util/ZapFormatter.{showAmount,toZapAmount}`; delete
  `formatZapAmount` and `formatSats` desktop-local helpers.
  `WalletColumnScreen.formatSats` intentionally kept — locale-aware full
  precision for wallet balance is by design.

Plan: docs/plans/2026-06-02-fix-desktop-feed-review-findings-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 13:45:00 +03:00
Róbert NagyandGitHub 1f81a7fb25 Merge branch 'main' into fix/desktop-log-noise 2026-06-02 10:51:59 +03:00
nrobi144andClaude Opus 4.6 2ca8eb31dc fix: address root causes of 6 runtime log noise issues
1. LocalRelayStore: use batchInsert() with per-row savepoints instead of
   manual transaction — UNIQUE constraint violations skip that row instead
   of failing the whole batch

2. Robohash empty hex: guard blank input in CachedRobohash.get() with a
   fallback all-zeros hex key instead of passing empty string to assembler

3. GiftWrapEvent decrypt: downgrade from WARN to DEBUG — expected when
   gift wraps from local relay cache aren't addressed to current user
   (subscription filter is correct, but hydration doesn't filter by p-tag)

4. Relay URL %20: decode percent-encoded spaces before rejection check in
   RelayUrlNormalizer.fix() — wss://relay.example.com/%20 now normalizes
   to wss://relay.example.com/ instead of being rejected

5. NIP19 Parser: downgrade from ERROR/WARN to DEBUG — malformed bech32
   from relay content is expected in the wild, catch+log is correct

6. VLC macOS: add --avcodec-hw=none (disables VideoToolbox that causes
   CVPN chroma failures) and --reset-plugins-cache (rebuilds stale cache
   on startup instead of logging hundreds of stale-cache errors)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:47:58 +03:00