Commit Graph
758 Commits
Author SHA1 Message Date
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
Róbert NagyandGitHub 70636c0f9a Merge branch 'main' into feat/desktop-feed-ui-refresh 2026-06-02 10:01:10 +03:00
Claude 77d8657b62 feat: page DM history by until+limit per relay (gap-proof stop signal)
The time-slice history bounded re-downloads but couldn't tell "this relay is
empty" from "this is a gap" — an empty time slice can sit above older messages,
so the only stop was the 10-year maxLookback, and a wide late slice could pull a
20k-event firehose in one request.

History now pages backward by until+limit, per relay (UntilLimitPager). Each
round asks every not-yet-empty relay for up to 10000 events older than its own
cursor, no since, so gaps are skipped: an empty page + EOSE is a gap-proof
"nothing older on this relay" signal. A relay returning fewer than the limit is
treated as its own cap, not exhaustion — only an empty page ends it. A relay
answering CLOSED isn't "empty" (it may answer after the auth handshake), so the
global exhausted flag flips only when a whole round advances no relay at all,
which also stops the loop on a relay that keeps CLOSing. limit caps per-request
volume too.

Both NIP-04 history managers now paginate themselves (per relay, scoped) instead
of following the gift-wrap slice; loadEverything pages to the end by auto-issuing
the next round until exhausted. The live tail and the rooms-list stall-gate are
unchanged. Filter builders gained an optional limit; the conversation NIP-04
helper exposes its outbox relay set + a per-relay until builder.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 03:54:09 +00:00
Claude 793860170f feat: split DM loading into a live tail + bounded history slices
Every widen re-requested the whole DM window (the filters carried `since` only,
no `until`), so a relay re-streamed the entire history from the new floor — a few
pixels of scroll walked the window to the 10-year backstop, re-downloading
exponentially more each step (589 → 1486 → 2609 events in one session). This
splits each DM protocol into two responsibilities:

- Live tail (existing managers, now fixed): a one-week floor with no `until`,
  always open to the future. Never widens, so new messages keep arriving.
- History slices (new managers): load the past in bounded `since`+`until`
  one-shot slices. Widening fetches only the new band `[newFloor, prevFloor]`;
  consecutive slices are disjoint so advancing the filter never re-streams an
  earlier slice — they live in the cache. The NIP-17 2-day wrapper-timestamp
  margin is applied to the slice `since`, overlapping adjacent slices so a
  randomized outer timestamp can't open a gap. NIP-04 (exact timestamps) needs
  no margin.

New: AccountGiftWrapsHistoryEoseManager owns the geometric window and the
bounded slices; ChatroomListNip04HistorySubAssembler / ChatroomNip04History-
SubAssembler follow its slice bounds so both protocols page to the same depth.
The live managers (AccountGiftWrapsEoseManager and the NIP-04 followers) are
reduced to the fixed one-week tail.

Also adds the rooms-list stall-gate: the auto-fill remembers the private-room
count at the last widen (on the history manager, so it survives reopening the
screen) and stops widening once a step brings in no new private room — widening
pulls older messages, not rooms, so a few busy correspondents would otherwise
flood events without ever filling the list. "Fill until full OR nothing new
found", instead of walking to the 10-year backstop.

Design: amethyst/plans/2026-06-01-dm-live-tail-and-history-slices.md

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-01 23:43:02 +00:00
Claude 9118a757e3 Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-01 19:38:01 +00:00
nrobi144andClaude Opus 4.6 9194dac8f9 feat(desktop): related content section in thread view
- Create CompactNoteData @Immutable data class in commons for reuse
- Create RelatedContentSection composable with horizontal LazyRow
- Scan LocalCache for hashtag-matching + same-author notes
- Compact cards (160dp) with title, author, zap count
- Wire into ThreadScreen below reply notes
- Hidden when no related content found
- Subscriptions cancel on dispose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 11:07:31 +03:00
Claude f26c00add0 refactor(commons): make HtmlParser KMP — drop java Charset dependency
Follow-up to the link-preview move: HtmlParser + HtmlCharsetParser were
stuck in jvmAndroid only because they spoke java.nio.charset.Charset.
There is no common Charset type in the Kotlin stdlib, so this reshapes
the API to speak IANA charset *names* (String) and pushes the single
genuinely-platform operation — byte->String decode — behind expect/actual.

- Move HtmlParser + HtmlCharsetParser to commonMain. Charset detection
  (meta-tag sniff + BOM sniff) is pure string/byte work; BOM detection no
  longer needs okio (manual leading-byte compare).
- Add `expect fun decodeBytes(bytes, charsetName)`:
    * jvmAndroid actual -> java.nio.charset (all JRE charsets, UTF-8 fallback)
    * iosMain actual -> NSStringEncoding for the common web charsets
      (UTF-8/16/32, Latin-1, CP1252, ASCII), UTF-8 fallback for the rest.
- UrlPreview (stays jvmAndroid; needs OkHttp) now reads response.body.bytes()
  and passes mimeType.charset()?.name().

Verified: commons compiles for JVM AND iosSimulatorArm64, verifyKmpPurity
passes, commons jvmTest passes, amethyst play + fdroid compile.
2026-05-30 23:34:05 +00:00
Claude 58ad87c900 refactor(commons): move link-preview fetcher to commons
Third slice of the amethyst→commons migration. UrlPreview (OpenGraph
link-preview fetcher) and HtmlParser already wrapped the extracted
commons preview parsers (MetaTagsParser/OpenGraphParser/HtmlCharsetParser);
this consolidates the whole link-preview concern in commons.

- Move service/previews/{UrlPreview,HtmlParser} into commons jvmAndroid
  service preview package. They land in jvmAndroid (not commonMain)
  because UrlPreview uses OkHttp and HtmlParser uses java.nio.charset —
  both JVM-only. No Android-framework or keystone coupling: the caller
  injects the OkHttpClient as a lambda.
- Add explicit okhttp + okhttp-coroutines deps to commons jvmAndroid
  (previously only present transitively via coil-okhttp).
- Re-point the single caller (model/UrlCachedPreviewer).

commons JVM compile + verifyKmpPurity pass; amethyst play + fdroid compile.
2026-05-30 22:57:37 +00:00
Claude 5f1514a152 refactor(commons): move relay broadcast tracker to commons
Second slice of the amethyst→commons migration. BroadcastTracker +
BroadcastEvent/RelayResult/BroadcastStatus are platform-agnostic relay
event-broadcast logic (no keystone coupling, no Android) that Desktop and
the CLI can reuse.

- Move service/broadcast/{BroadcastModels,BroadcastTracker} into
  commons commonMain service/broadcast.
- Replace the two commonMain purity-gate violations:
  System.currentTimeMillis() -> TimeUtils.now() (startedAt is only used
  to sort the active-broadcast list) and java.util.UUID.randomUUID() ->
  RandomInstance.randomChars(16) for the tracking id.
- Re-point the 4 Android callers (AccountViewModel + broadcast UI).

verifyKmpPurity passes; amethyst play + fdroid both compile.
2026-05-30 22:42:05 +00:00
Claude e66c27e375 refactor(commons): move CLI-safe util extensions out of amethyst
First low-friction slice of the amethyst→commons migration
(commons/plans/2026-05-30-amethyst-to-commons-migration.md): the model
nipNN state holders are all blocked by the LocalCache/Note/Account
keystone (Phase A), so start with the genuinely Android-free utilities.

- Delete amethyst service/IterableExt.kt — exact duplicate of the existing
  commons util/IterableUtils.kt (Iterable.replace); re-point 4 callers.
- Move retryIfException (CoroutinesExt.kt) into commons util/CoroutinesUtils.kt.
- Move togglePresenceInSet (SetExt.kt) into commons util/SetUtils.kt.

All commonMain-safe (verifyKmpPurity passes). amethyst play + fdroid both
compile against the relocated helpers.
2026-05-30 22:23:47 +00:00
Claude 6f9c5bbdf0 fix: complete DM windows on quiescence, add load-entire-history button
The fixed 15s window-load timeout fired mid-flood on accounts with a large
DM history: a relay streaming thousands of stored gift wraps never EOSE'd
within 15s, so the window was declared "loaded" while events were still
pouring in and before they were decrypted into rooms. The rooms list still
looked empty, so auto-fill widened again — re-issuing an ever-wider REQ that
re-downloaded the whole history, over and over, every 15s.

WindowLoadTracker now completes a window on activity quiescence instead of a
wall clock: it stays loading until every expected relay EOSEs, or the event
stream goes quiet for a few seconds. Every event (stored backfill included)
bumps the idle timer via onActivity, so a relay mid-flood is never mistaken
for a finished window; an absolute cap bounds pathological dribble. Both DM
loaders feed event activity in (the NIP-04 loader now uses a custom listener
so it sees stored events, not just live ones).

Also add a "Load entire history" button to the rooms-list footer: it jumps
the window straight to the max lookback (TimeWindowPagination.loadAll) so a
single REQ pulls everything — the pre-windowing behavior — and marks the
window exhausted so the auto-fill loop stops.
2026-05-30 22:05:57 +00:00
Vitor PamplonaandGitHub 4ae606805f Merge pull request #3112 from vitorpamplona/claude/gracious-cori-uLr4P
Move NIP-51/72 decryption caches and models to commons
2026-05-30 18:04:49 -04:00
Claude abc9cd14cf refactor: move InterestSet to commons
Pure data class (only @Stable) — extract to
commons/model/nip51Lists/interestSets so Desktop/CLI/iOS can reuse the
interest-set model. Re-points the four interest-set UI files and the
sibling InterestSetsState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:59:26 +00:00
Claude a38388fce5 refactor: move LabeledBookmarkList to commons
Pure data class (only @Stable + quartz bookmark tags) — extract to
commons/model/nip51Lists/labeledBookmarkLists so Desktop/CLI/iOS can
reuse the bookmark-group model. Re-points the six bookmark-group UI
files and the sibling LabeledBookmarkListsState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:52:06 +00:00
Claude 593c320004 refactor: move Mute/People/Community decryption caches to commons
Extract the three remaining keystone-free quartz-only decryption caches
(MuteListDecryptionCache, PeopleListDecryptionCache,
CommunityListDecryptionCache) into commons/model so Desktop/CLI/iOS can
reuse them. Re-points Account, FeedDecryptionCaches, and the sibling
state holders (MuteListState, PeopleListsState, BlockPeopleListState,
CommunityListState).

The remaining relay-list decryption caches depend on
GenericRelayListCache -> amethyst.model.Note (the keystone), so they
stay until Phase A extracts Note/LocalCache.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:46:13 +00:00
Claude d311a01964 feat: auto-fill and prefetch the rooms list with a growing DM window
Replace the fire-once scroll detector with a viewport-fill + prefetch loop
so the messages screen stays ahead of the user instead of stranding a
near-empty list.

One condition drives three behaviors: widen the DM time windows when the
feed is empty, or when the last visible row crosses the midpoint of what's
loaded. While the list is short everything is visible, so the midpoint is
always crossed and it keeps widening until the list overflows the viewport
with a buffer below the fold; once full it only fires again as the user
scrolls past the new midpoint, so a fresh chunk lands well before the end.
It stops only when the window is exhausted (reached the 10-year lookback —
nothing older exists), which also gives the empty-account case a real
terminating condition instead of the old runaway cascade.

- TimeWindowPagination: optional geometric step growth + a hard max-lookback
  floor with isExhausted(), so a sparse / single-person history converges in
  ~10 requests. Default stays linear/unbounded; existing callers unchanged.
- WindowLoadTracker: a window counts as loaded only once ALL of its relays
  have answered (EOSE / live event) or a timeout fires — not on the first
  EOSE. This stops a fast, near-empty relay from clearing the gate and
  letting the fill loop outrun the slow relay that holds the conversations.
- Both DM loaders (NIP-17 gift wraps + NIP-04) expose loadingMore (= window
  still loading) and exhausted, advance in lockstep, and gate each widen on
  the tracker. The rooms screen shows a spinner until history is exhausted
  rather than flashing the empty state while older windows are still in
  flight.
2026-05-30 21:44:26 +00:00
Claude ab72427445 refactor: move HashtagListDecryptionCache to commons
Pure quartz-only decryption cache — extract to
commons/model/nip51Lists/hashtagLists. Re-points Account,
FeedDecryptionCaches, and the sibling HashtagListState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:43:55 +00:00
Claude 466086c475 refactor: move FavoriteAlgoFeedsListDecryptionCache to commons
Pure quartz-only decryption cache — extract to
commons/model/nip51Lists/favoriteAlgoFeedsLists so Desktop/CLI/iOS can
reuse it (mirrors the TrustProviderListDecryptionCache move). Re-points
Account and the sibling FavoriteAlgoFeedsListState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:42:51 +00:00
Claude c619d71890 refactor: move NwcWalletEntry to commons/model/nip47WalletConnect
Pure data class (only quartz + a UUID id generator) — extract to
commons so Desktop/CLI/iOS can reuse the NWC wallet entry model.
Swaps java.util.UUID for the multiplatform kotlin.uuid.Uuid (matching
the quartz convention) and re-points the three callers
(LocalPreferences, AccountSettings, WalletViewModel).

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:40:54 +00:00
Vitor PamplonaandGitHub 34f6b2ba36 Merge pull request #3111 from vitorpamplona/claude/epic-hamilton-23225
NIP-32: Add hashtag labeling and label-based hashtag feed
2026-05-30 17:32:03 -04:00
Claude b0f9b1621a refactor: move CashuToken to commons/model/nip60Cashu
CashuToken and Proof are pure data classes (only @Immutable +
kotlinx.serialization). Extract to commons so Desktop/CLI/iOS can reuse
them, and re-point all callers.

https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod
2026-05-30 20:37:04 +00:00
Claude d57f8c18c6 feat: first-class NIP-32 hashtag labels on posts and in the hashtag feed
Let users tag any post with a hashtag via a NIP-32 kind 1985 label event
(using the `#t` tag-association namespace), and surface follow-labeled
posts in the hashtag feed.

quartz:
- LabelEvent.buildHashtagLabel() + HASHTAG_NAMESPACE ("#t") and
  hashtagAssociations() to build/extract hashtag-association labels.

commons:
- Note now carries a `labels` reverse-reference map (hashtag -> labeler
  notes) with addLabel/removeLabel and a NoteFlowSet.labels flow,
  mirroring reactions/reports.

amethyst:
- LocalCache consumes LabelEvent, attaching hashtag labels to their
  target notes and re-notifying feed observers for already-cached
  targets.
- Account.createLabelHashtagEvent/labelHashtag/consumeLabelEvent and
  AccountViewModel.labelWithHashtag (tracked + direct broadcast).
- Overflow "⋯" menu gains an "Add hashtag" action backed by a new
  AddHashtagLabelDialog.
- HashtagFeedFilter also accepts posts a followed user labeled with the
  hashtag; a new label sub-assembler subscribes to kind 1985 by `#l`
  and fetches missing label targets.
- Hashtag feed shows an attribution banner ("#tag added by @user") above
  follow-labeled posts via a custom RefresheableFeedView onLoaded.

https://claude.ai/code/session_019gc3FipVBcndF9fmqCCfVX
2026-05-30 20:32:43 +00:00
Claude 19d6c9baaa refactor: move TrustProviderListDecryptionCache to commons, rename pkg to nip85TrustedAssertions
Extract the pure (quartz-only) decryption cache into commons. Converge
the commons trustedAssertions package onto the quartz NIP slug
'nip85TrustedAssertions' (per migration plan §10.2), moving the existing
TrustProviderListState interface + UserCardsCache with it and
re-pointing all callers.

https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod
2026-05-30 20:15:36 +00:00
Claude 47a70b4fa3 refactor: move OwnedEmojiPack to commons
Pure data class (only quartz + @Stable deps) — extract to
commons/model/nip30CustomEmojis so Desktop/CLI/iOS can reuse it.
Moves the unit test to commons commonTest and re-points callers.

https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod
2026-05-30 20:15:28 +00:00
Claude d8899eedf8 refactor(commons): move feature-specific UI out of ui/ into <feature>/ui
Make the feature-UI vs cross-cutting-UI rule consistent (feature-first):
- ui/nip53LiveActivities -> nip53LiveActivities/ui
- ui/article + ui/editor -> new nip23LongContent/ui (article reader + editor)

ui/ now holds only cross-cutting composables (theme, components, layouts,
elements, markdown, signing, thread, feeds, notifications, screens, state,
text). Tighten ARCHITECTURE.md with the deciding test ('could a second
unrelated feature reuse this as-is?') and reconcile the NIP-second-axis
section so a single-NIP feature owns its UI under <feature>/ui rather than
ui/nipNN.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 19:02:55 +00:00
Claude 79a9bf78f8 refactor(commons): name single-NIP feature packages after their quartz NIP
Rename the two single-NIP feature packages to mirror their quartz
counterparts for 1:1 traceability:
- chess -> nip64Chess
- call  -> nipACWebRtcCalls

marmot and nip53LiveActivities already match quartz and are unchanged.
Document the rule in commons/ARCHITECTURE.md: layer is the primary axis,
NIP is the secondary axis (nipNN<slug> matching quartz), and commons is
deliberately NOT reorganized NIP-first at the top level. Also remove
stray markup that leaked into the end of the doc.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 18:46:36 +00:00
Claude b0c6ffb821 refactor(commons): consolidate package taxonomy + add architecture doc
Document the commons module's purpose, source-set layout, and the CLI-safe vs
UI boundary in commons/ARCHITECTURE.md, then clean up the clearest package
overlaps that had accumulated:

- merge duplicate util/utils -> util (all source sets)
- unify service/services -> service (jvmAndroid)
- move data/UserMetadataCache -> model/cache
- fold compose/ into ui/ (ui/article, editor, elements, layouts, markdown,
  nip53LiveActivities, and Compose helpers in ui/state + ui/text)
- move ProfileBroadcastBanner composable into profile/ui

All changes are whole-file/whole-package moves with import rewrites; no logic
changed. The chess logic/UI split is documented as deferred debt (it needs
file-level surgery, not moves). Marks docs/shared-ui-analysis.md superseded.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 17:03:57 +00:00
Claude 43744e53ad feat: bound DM boot loading to a time window with scroll-to-load-more
The always-on gift-wrap subscription (AccountGiftWrapsEoseManager) had no
lower bound on first boot: `since` came purely from the per-relay EOSE
cursor, which is null on a cold start, so every DM relay dumped the account's
entire NIP-17 history at once — all of which then had to be unwrapped and
NIP-44 decrypted before the messages list felt usable.

Replace that with a per-account time window:

- New `TimeWindowPagination` primitive (commons): tracks a moving `since`
  floor, opens a small window at boot, widens backward one step per
  `loadMore()`. The subscription stays open so live messages still stream in
  regardless of the window.
- `AccountGiftWrapsEoseManager` now requests gift wraps from the window floor
  instead of the EOSE cursor, exposes `loadMore(user)` and a `loadingMore`
  flag, and clears the flag on EOSE.
- The rooms list (`ChatroomListFeedView`) widens the window when scrolled near
  the end and shows a loading footer while the next window loads. It
  re-evaluates as the list grows so a near-empty first screen keeps filling.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-05-30 14:43:45 +00:00
Claude 93b77348ac refactor: enlarge the Cashu sunglasses further
Bump the pixel sunglasses scale (1.45 -> 1.70) so the shades read bolder and
overhang the slimmed, rounded cashew body more prominently. Body outline and
1.2 stroke weight unchanged.
2026-05-29 23:30:48 +00:00
Claude df33b99b07 refactor: round the Cashu nut body into a smooth outline
Replace the blocky pixel-stepped cashew silhouette with a smooth rounded
outline (corner-cut and emitted as a compact Bézier spline) so the nut reads
as a clean curved shape instead of a staircase. Use a round stroke cap/join at
the same 1.2 weight; the pixel "deal-with-it" sunglasses stay a solid fill.
2026-05-29 23:01:34 +00:00
Claude 71bed1cf0a refactor: slim Cashu body and enlarge the sunglasses
Squeeze the cashew outline horizontally (~0.72) and scale the pixel sunglasses
up (~1.45) so the shades overhang the body for a bolder, more recognizable
mark. Stroke weight stays at 1.2 to match the shared Zap outline icon.
2026-05-29 22:51:54 +00:00
Claude 9010e85411 feat: monochrome outline Cashu icon for Material Symbols compatibility
Convert the multi-tone "deez nuts" Cashu/nutzap logo into a single-color,
tintable outline icon so it behaves like a Material Symbol glyph: the cashew
body is a hollow stroke (1.2 weight, matching the shared Zap outline icon) and
the pixel sunglasses stay a solid fill so they read at small sizes.

Drop the `tint = Color.Unspecified` overrides at every call site (zap chips,
nutzap rows/gallery, redeem, wallet screens) so the icon now tints with the
surrounding content colour instead of being locked to the brand browns, and
remove the imports/comments that only existed to preserve the old multi-tone
rendering.
2026-05-29 22:38:43 +00:00
Claude 30a845a6c1 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-amethyst-sdOWe
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
2026-05-29 13:32:49 +00:00
Vitor Pamplona c1dd59a068 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/LifecycleAwareKeyDataSourceSubscription.kt
2026-05-29 08:57:31 -04:00
Vitor Pamplona c1c4f7f72a Uses the parent note's p-tags to load children notes that cannot be found 2026-05-29 08:47:28 -04:00
Vitor Pamplona ca8589b29c Quick check to make sure the gatherers are not duplicated 2026-05-29 08:37:08 -04:00
Vitor PamplonaandGitHub fd88e2f8a5 Merge pull request #3095 from vitorpamplona/claude/amazing-ptolemy-26Nek
Use locale-aware date/time formatting throughout the app
2026-05-29 08:30:41 -04:00