Commit Graph
14594 Commits
Author SHA1 Message Date
Claude edbdbdcc8e fix: give up on unreachable relays so the rooms list resolves
Follow-up to the no-progress retry: in this trace vitor/mom/nos.lol never
connect at all (only damus gets a REQ — the other three cannot-connect
for both live and history). The retry correctly re-attempts, but it can't
reach relays that won't connect, so every round is 0 events, no relay is
ever 'done', exhausted never completes, and it retries forever — a cold,
empty feed stays on the spinner.

The round-model history had no give-up for repeated cannot-connect (only
for CLOSE), unlike the convo path. Route onCannotConnect through the same
pager give-up as onClosed, so a relay that's unreachable for a few rounds
is abandoned, exhaustion completes, and the screen resolves (to the
loaded rooms, or empty + retry) instead of spinning forever. The streak
resets on any contact, so a merely slow relay that connects within a few
attempts isn't dropped. Applied to both round-model managers.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 21:52:38 +00:00
Claude a3b4df8e6e fix: rooms-list history stuck on 'Loading feed' after a no-progress round
On a cold start with no cached rooms and no recent (<7d) DMs, the rooms
list relies entirely on history paging. If a round settles via
cannot-connect / CLOSE (relays dropped during a connect storm) instead of
a clean empty-EOSE, no relay is marked done, exhausted stays false, and
the no-progress guard then refuses to retry — recovery would only come if
the relays happened to reconnect AND re-EOSE the open subscription on
their own. Meanwhile the empty feed shows LoadingFeed() forever (it needs
BOTH protocols exhausted to show 'no conversations').

When a round makes no progress but isn't exhausted, actively retry after
a 5s backoff (clearing the no-progress gate) instead of waiting
passively. Paced so a fast-CLOSE / rate-limited relay isn't hammered, and
it stops once the protocol exhausts or a round makes progress. Applied to
both round-model history managers (giftwrap + rooms NIP-04).

Not caused by the stall-gate drop — the empty-feed search path is
unchanged by that commit; this is a pre-existing fragility the severe
76s connect storm in this cold start exposed.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 21:34:50 +00:00
Claude 98fb872005 feat: drop the rooms-list stall-gate — page to exhaustion while in view
The stall-gate stopped widening once a load surfaced older messages but
no new conversation row, leaving the boundary card in a confusing paused
state (not loading, not caught-up). It existed to brake the OLD
pagination model, where every widen re-downloaded the whole window; the
per-relay until+limit paging doesn't re-download, so the brake is
obsolete — and a visible boundary card means the user is waiting for
more, so pausing there made no sense.

Now while the boundary is in view each protocol pages round after round
until genuinely exhausted (empty page), then shows 'all caught up'. The
card is only ever loading or caught-up, never paused. Removes the
autoFillRoomMark mark plumbing and the getMark/setMark gate.

Tradeoff: a user with few conversations but deep message history pages
that history to the end on reaching the bottom — bounded and efficient
now (no re-download), terminated by exhaustion + the no-progress guard.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 20:57:27 +00:00
Claude 432a19f7c0 feat: label the in-stream relay markers 'Relay sync:'
The bare glyph markers (✓ 8 · ↓ 1) had no context. Prefix each with a
translatable 'Relay sync:' label and add '·' separators between states,
so a marker reads e.g. 'Relay sync: ✓ 8 · ↓ 1' or 'Relay sync: ↓ nostr.wine'.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 20:25:57 +00:00
Claude 81dd2585dc fix: DM history card showed text but a blank icon when paused
The status card's icon slot only handled two states — caught-up (✓) and
loading (spinner) — leaving it blank in the third: not exhausted but not
actively loading (the rooms-list auto-fill stops short of exhaustion via
the no-new-rooms stall-gate, or between round-model pages). The card then
read 'Older … messages · N relays' with nothing on the left. Fill that
paused state with a static '⋯' glyph so the slot is never empty.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 20:19:44 +00:00
Claude 40b60e7bcc fix: compact relay markers + no '0 relays' flash on the DM loading card
Two UI papercuts in the per-relay DM history surface:

- RelayReachMarker listed every relay name comma-joined per state, so a
  gap shared by many relays (e.g. all nine clustered at the live-tail
  floor on first open) overflowed into an unreadable line. Now each state
  shows the relay's host name only when it is the sole one of its state at
  that depth (the usual converged case); otherwise just a count, with
  maxLines/ellipsis as a backstop.
- The history status card briefly read 'loading from 0 relays': the count
  populated a beat after loadingMore flipped true (and again as the last
  relay settled). Set the relay count before raising the spinner in
  loadMore, and defensively drop the relay clause from the card subtitle
  when the count is 0.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 18:50:13 +00:00
Claude ab7aa44d21 fix: pin the convo history floor per window to stop re-REQ churn
startUntil() is 'now - 1week' and was recomputed on every updateFilter,
so it drifted forward in real time. A relay that hadn't advanced its
cursor (first page in flight, or empty) has until = that floor, so its
filter changed every time ANY other relay's EOSE triggered
invalidateFilters — the subscription saw a 'new' filter and re-REQed it.
The trace showed nostr.oxtr.dev asked twice (until 1779903206 then
...207, +1s) and reaching the bottom (done) twice, and it fed extra
ditto REQ->CLOSE churn and rate-limiting on the busy relays.

Pin the floor once when the window starts and reuse it for the window's
life (which ends in ~2s once every relay is done or stalled), so an
un-advanced relay's filter stays stable and only a relay whose cursor
genuinely advanced is re-REQed.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 17:41:45 +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 5b0f2051db fix: convo NIP-04 history stuck 'loading' with 0 relays on first open
loadingMore was wired straight to windowLoad.loading, which starts true
(the tracker assumes a load is in flight from construction). On the
first conversation open — before any paging window has run — that true
wedged the scroll-driven loader: its gate is '!loading', so loadMore
never fired, and even if it had, the 'if (!windowLoad.loading.value)
startLoading' guard would have skipped startLoading (value was the
construction-time true), leaving no watchdog to ever settle it. Result:
permanent spinner, 0 relays. Earlier opens only worked because a prior
conversation had left the shared tracker at false.

Expose a _loadingMore that starts false and is mirrored from the window
by the done collector, and track windowActive ourselves so the first
loadMore actually starts the window (and a re-entrant loadMore mid-page
doesn't reset it and forget finished relays).

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 16:48:18 +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 9423463f17 Merge pull request #3129 from vitorpamplona/claude/sleepy-sagan-Q2p3t
Register CommunityRulesEvent in EventFactory for kind 34551
2026-06-03 09:35:28 -04:00
Claude 3a43c4f7bb fix: register CommunityRulesEvent in EventFactory
Kind 34551 (CommunityRulesEvent) was missing from EventFactory.create, so
signing a community-rules template produced a generic Event. Returning it
as CommunityRulesEvent in Account.sendCommunityRules threw a
ClassCastException when publishing community rules.

Register the kind in the factory and add a regression test.
2026-06-03 13:30:52 +00:00
David KasparandGitHub 479c694e6e Merge pull request #3128 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-03 14:35:31 +02:00
Crowdin Bot 6f645511f0 New Crowdin translations by GitHub Action 2026-06-03 12:02:06 +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
Vitor PamplonaandGitHub 74af87ae37 Merge pull request #3127 from davotoula/feat/settings-search
Searchable, data-driven settings screen
2026-06-03 07:58:50 -04:00
davotoulaandClaude Opus 4.8 72d538bb62 refactor(settings): address review — non-translatable keywords, symEntry helper, legal keywords
- Mark all *_search_keywords translatable="false" (English concept/protocol
  index; stops Crowdin translating protocol terms and breaking locale search) [#1]
- Collapse ~23 symbol+nav rows via a local symEntry() helper [#3]
- Reword keywordsRes KDoc to match the actual word-prefix tokenization [#6]
- Add search keywords to the Legal rows (privacy_policy, child_safety) [#7]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:08:28 +02:00
davotoula d75e80bea6 feat(settings): curated search keywords for all rows + word-prefix search matching
Add keyword blobs so settings resolve by concept/protocol name, not just
title — e.g. "blossom" -> Media Servers, "audio rooms" -> Nests Servers,
"negentropy" -> Event Sync, "nsec" -> Backup Keys. NIP numbers omitted by
preference.
2026-06-03 09:45:20 +02:00
David KasparandGitHub fc14ad6bfc Merge pull request #3126 from nrobi144/feat/desktop-new-posts-chip
feat(desktop): home-feed scroll polish + sidebar tooltips
2026-06-03 09:43:05 +02:00
davotoula d8e4ada781 Code reviewP
- Collapse filterSettings' two identical lookup lambdas into one stringLookup
- Rebuild via SettingsCategory.copy() so new fields aren't silently dropped
- Memoize buildSettingsCatalog with remember(hasPrivateKey, nav, uriHandler);
  onResetMarmot reads isResettingMarmot via rememberUpdatedState to avoid a
  stale-capture, eliminating ~60 allocations per keystroke
2026-06-03 09:21:24 +02:00
davotoula 4446b12972 feat(settings): add settings catalog data model + filterSettings
feat(settings): add search box that filters settings rows by title + keywords
refactor(settings): expose legal section as legalSettingsCategory factory
feat(settings): add search placeholder, empty-state, and keyword strings
test(settings): cover filterSettings (blank/title/keyword/category/empty/danger)
refactor(settings): match category title in search; use data classes
2026-06-03 09:20:54 +02:00
nrobi144 599a16193a fix(desktop): collapsed sidebar — tighter ripple + hover tooltip
Two related polish fixes on the collapsed sidebar:

1. The hover/active highlight on each nav item used to span the full
   sidebar width (minus 8dp outer padding), producing ~12dp of empty
   highlight either side of the 24dp icon. Now the highlight clips to
   a 40dp square centered on the icon (24dp icon + 8dp padding on each
   side), so the ripple sits tight against the glyph.

2. When the sidebar is collapsed, the label was already supplied as
   `contentDescription` for screen readers but had no visual
   affordance. Added a `TooltipArea` that surfaces the label on hover
   (Surface + inverseSurface tonal style, matching the existing
   TorStatusIndicator tooltip pattern), so mouse users can also see
   what each icon means without expanding the sidebar.

Applied to both `SidebarNavItem` and `SidebarFeedItem` since both
suffer the same issue. Expanded behaviour is unchanged.
2026-06-03 09:46:15 +03:00
nrobi144 e5b210d4e1 fix(desktop): make first-pinned-feed default actually take effect
Two bugs that together caused HomeFeed to always open on Following:

1. FeedScreen was reading feedRepo.pinnedFeeds.value as the source of
   truth for the first pinned feed. That's a stateIn-derived flow with
   initial value persistentListOf(); the underlying _feeds StateFlow
   IS loaded synchronously by FeedDefinitionRepository on construction,
   but the derived pinnedFeeds doesn't reflect it until the first flow
   emission propagates — which is too late for `remember` to see.
   Fixed by reading feedRepo.feeds.value directly and filtering /
   sorting by pinOrder ourselves.

2. DeckColumnContainer was passing initialFeedMode = FeedMode.FOLLOWING
   when rendering DeckColumnType.HomeFeed, which overrode FeedScreen's
   first-pinned logic entirely. Removed the hardcode so the deck's
   home column inherits FeedScreen's default.

With both fixed, a user who has only Global pinned now opens to Global
on launch instead of Following.
2026-06-03 09:36:18 +03:00
nrobi144 99af0f75e1 fix(desktop): default home tab to first pinned feed, not last-saved mode
If the user has pinned only Global (or only a custom feed), the app
should open to that on launch instead of showing Following just
because DesktopPreferences.feedMode happened to be saved as
Following. The "pinned feeds" list is the user's stated ordering;
the first item should drive the initial tab.

Resolution order (most specific wins):
  1. explicit customFeedSource/customFeedId from the caller
  2. explicit initialFeedMode from the caller
  3. first pinned feed in feedRepo.pinnedFeeds (NEW)
  4. DesktopPreferences.feedMode (last-saved, previous default)

For a pinned Filter feed, this also seeds activeFeedId and
activeFeedSource so the feed mounts in CUSTOM mode with the right
source.
2026-06-03 09:31:02 +03: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
nrobi144 44febcc77f feat(desktop): add Amethyst logo to Tor and account-loading splashes
Both loading splashes (the Tor-connect gate and the account-loading
screen between Tor active and LoginScreen) now show the Amethyst
icon tinted to the theme primary, anchored below the status text.

Layout pattern (status-forward, both splashes):
  spinner → status text → Amethyst logo (96.dp, primary tint)

Brief research summary backing the choice:
- Apple HIG argues against splash branding, but its model assumes
  near-instant launch — not applicable here where the Tor gate
  can block for seconds.
- Material Design 2's branded-launch-screen pattern endorses
  logo + brand color while a placeholder UI loads.
- The status-forward order keeps the dynamic info (what we're
  waiting on) leading and the brand as the anchor below — the
  right call when the wait is non-trivial.
2026-06-03 07:31:49 +03:00
nrobi144 38a191341f fix(desktop): bump new-posts chip top margin to 16dp
Tighter 8dp gap clipped visually too close to the search header card.
2026-06-03 07:31:34 +03:00
Claude af193e03ab refactor: tidy DM history assembler + restore per-relay diagnostics
Audit follow-up on the per-relay paging work:

- Restore DmRelayLog in the convo history loadMore (every other nip04 /
  giftwrap assembler logs it) and add per-relay milestone logs: which
  relay reached the bottom, which stalled and why, plus a one-line
  done/still-trying breakdown when the window settles — the snapshot to
  reach for when a chat doesn't load.
- Extract markStalled() (dedupes onClosed/onCannotConnect, logs once per
  relay) and relaysFor() (dedupes the active-convo relay lookup).
- relayCount now counts the relays still being paged (done ones drop out)
  instead of staying frozen at the total.
- Drop the unused loadEverything().
- Fix WindowLoadTracker docs/log that claimed it 'gives up' on silent
  relays: it only stops waiting and reports them; the owner decides (the
  convo keeps them open and retries). Fix a dangling KDoc link in
  RelayReachMarker.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 02:26:46 +00:00
Claude afa02df3a0 feat: in-stream per-relay paging markers for NIP-04 conversations
Visualises the per-relay-independent engine: each relay gets a thin
marker in the message stream at the depth (createdAt) it has paged down
to, sitting just below the oldest message it has loaded. As a relay
pages older its reached-back cursor drops and the marker slides down; a
relay that races ahead leaves its marker deep while slower relays' trail
higher and converge as they catch up — ✓ done (empty-EOSE), … stalled
(auth CLOSE / unreachable, still trying), ↓ reaching. Hidden once the
conversation is fully converged (every relay done or stalled).

Adds an optional markersInGap slot to the shared chat feed view
(no-op for public-chat callers) invoked per message gap with its
createdAt bounds; ChatroomView renders the markers from the
relayProgress flow.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 00:39:00 +00:00
Claude cbee966bb7 feat: page each NIP-04 DM relay independently to converge on one window
Replaces the lock-step round model (every relay advanced one page per
global round, gated by the slowest) with per-relay continuous paging:
each relay continues to its next page the instant it EOSEs, off its own
cursor. The subscription layer diffs per relay, so re-issuing only
re-REQs the relay whose cursor moved — others' in-flight REQs are
untouched. Fast relays race to the bottom in back-to-back pages while
slow / auth-walled relays catch up at their own pace; none are
abandoned (this reverts the give-up behaviour — slow relays keep their
subscription open and keep trying), so every relay converges on the same
window.

A relay is done on an empty page; one that won't answer (auth CLOSE,
unreachable, silent) is marked stalled but keeps trying. loadingMore
clears once every relay is done or stalled. Exposes per-relay
RelayPagingProgress (reached-back / done / stalled) for the upcoming
in-stream progress markers.

Splits the convo widen loop so each protocol pages on its own loader
state — NIP-04's continuous loading no longer starves gift-wrap paging.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 00:29:53 +00:00
Claude 07b0c87c38 fix: stop giftwrap/rooms loads completing before their REQs are sent
The silence + connect-grace backstops are only meaningful when the owner
feeds onReqSent, which only the convo NIP-04 path does. But connectStalled
keyed off the ABSENCE of a recorded REQ, so for giftwrap/rooms (which never
call onReqSent) every relay looked connect-stalled after connectGrace —
the window completed at ~15s before its REQs had even gone out during a
slow connect storm ('giftwrap.live load done: settled/silent' 45s before
the REQ), prematurely declaring an empty round done and tripping the
no-progress guard, so giftwraps stopped loading.

Gate both REQ-aware backstops behind a tracksReqSends flag that only the
convo manager sets; everyone else keeps the plain settle / idle / cap
behavior. Adds a regression test that a non-tracking tracker keeps
blocking a never-heard-from relay until it actually settles.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 00:04:09 +00:00
Claude 0d27477c4d fix: mark a DM conversation done when its last relays stop making progress
After the connect-grace fix the convo history no longer hangs, but it
could linger forever showing 'N relays' with no progress bar and
exhausted=false: a correspondent's auth-walled relay (ditto, which only
ever CLOSEs 'all authors must be authenticated') never reaches the
pager's done/given-up state, and the no-progress guard merely *skipped*
re-issuing the round — stopping the loop without ever reflecting that
we're finished.

When the guard trips (same active relays, zero events two rounds
running) give up on those relays and recompute exhaustion, so the
conversation reports as fully loaded and the relay count clears. My own
reachable relays empty-EOSE to 'done' and never reach this branch, so
only genuinely stuck correspondent relays are dropped.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 23:43:12 +00:00
Claude d658ae2414 fix: don't let a relay stuck before its REQ hold the DM load for 5 min
The convo history spinner could sit for the full 5-minute absoluteCap
('load done: cap' in the logs). A correspondent relay (ditto) dropped
after its auth-required CLOSEs and got stuck reconnecting on a flaky
network, so its round-2 REQ was never delivered. It therefore reached
neither a terminal signal (no CLOSE without a REQ) nor the silence
backstop (which measures from REQ-delivery), and blocked the round until
the cap.

Add a connect-grace backstop: a relay that has been expected past
connectGrace (15s) without even receiving its REQ — i.e. stuck
connecting — stops blocking the round. Unlike the silence backstop it
does NOT give the relay up (it may be a genuinely slow connect), so the
owner keeps it and retries it next round; only relays that accepted a
REQ and then went silent are abandoned.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 22:31:42 +00:00
Claude 3cb6613cd7 fix: give up on DM relays that accept a REQ but never answer
The convo NIP-04 history spinner stayed up for minutes against 9 relays
even when the conversation was fully loaded. Auth-walled relays (ditto,
nostr.wine, …) accept the REQ (success=true) and then send nothing — no
event, no EOSE, no CLOSED. WindowLoadTracker only completed once every
relay reached a terminal signal, an idle gate that required hearing from
*all* relays, or the 5-minute cap; a silent relay armed none of those,
so only the cap freed the spinner. The pager likewise kept the silent
relay 'active' every round, so the count never dropped and exhaustion
never completed.

Add a silence backstop keyed off onSubscriptionStarted (REQ delivered,
post-connect — so a slow connect isn't mistaken for a dead relay): a
relay that received its REQ but stays silent past silenceTimeout (10s)
no longer blocks completion and is reported via onAbandoned, which the
convo assembler uses to giveUp() the relay in its pager so it leaves the
active set and lets exhaustion finish. finish() applies the give-up
before flipping loading, so the round collector recomputes exhaustion
after the silent relays are dropped.

Tests cover the pager give-up/exhaustion and the tracker's silence +
connection-gap behavior.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 22:10:16 +00:00
Claude 0430fd176c refactor: scope NIP-04 DM filters per relay to the keys that own each relay
Nip04DmRelays held two flat relay sets and every filter named the whole
conversation group, so a relay that belongs to only one counterpart was
still asked about all of them (e.g. {authors:[bob,charlie]} sent to a
relay that is only charlie's). Restructure it into per-relay key maps so
each relay sees exactly the keys it owns:

  fromMe: my outbox             -> {authors:[me], #p:[whole group]}
          each counterpart inbox -> {authors:[me], #p:[keys reading there]}
  toMe:   my inbox              -> {authors:[whole group], #p:[me]}
          each counterpart outbox-> {authors:[keys publishing there], #p:[me]}

Relays shared across roles union their key sets, so my own relays still
carry the full group while a counterpart's relay only ever names that
counterpart.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 21:48:06 +00:00
Claude 772355d2fc fix: don't ask a correspondent's relays for my own NIP-04 messages
A conversation's NIP-04 relay set folded the correspondent's inbox
(read) relays into the from-me filter set, so we sent {authors:[me]} to
relays that belong to the other party (e.g. ditto). Those relays have no
reason to hold my authored messages and auth-walled ones reject the
filter outright ("all authors must be authenticated"), stalling the
load. Scope filters to relay owners: my outbox carries my messages; the
correspondent's outbox (plus my own inbox as a legacy safety net)
carries theirs. Drops groupInbox from the from-me set.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 21:28:30 +00:00
Claude 5bd7b765d6 chore: log which account contributes which relays to the DM filters
Adds DmRelayLog, a diagnostic that prints — per DM subscription — the
account whose relays are being used and breaks the relay set down by the
source list each relay comes from (NIP-65 inbox/outbox, DM-relay-list,
private-storage outbox, local relays). Wired into all six DM assemblers
(NIP-17 live/history, NIP-04 rooms live/history, NIP-04 convo live/history).

The existing REQ lines now also print the resolved relay URLs (split into
fromMe/outbox and toMe/inbox for the NIP-04 paths), so an unexpected relay
— e.g. a write-only NIP-65 relay that only the NIP-04 home+dm path queries —
can be traced back to the list it leaks in from.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 20:57:34 +00:00
Claude 5080ab08c5 fix: give up on relays that keep rejecting us so a chat can finish loading
With "exhausted only when every relay empty-EOSEs", a relay that demands auth we
can't satisfy — e.g. relay.ditto.pub answering "auth-required: all authors must
be authenticated" for a correspondent's pubkey we can't authenticate as — CLOSEs
every round, never finishes, and the conversation loads forever.

UntilLimitPager now tracks a per-relay CLOSED streak; after GIVE_UP_AFTER_CLOSES
(3) consecutive CLOSEDs with no answer in between (so the pool's auth handshake +
a retry have already failed), the relay is marked "given up" and excluded from
activeRelays. It is NOT counted as done (it didn't empty-EOSE — we just can't
read it), but it no longer blocks exhaustion. The streak resets on any event or
EOSE, so a relay whose auth succeeds is never abandoned. onClosed wires into the
pager and re-checks exhaustion when a relay tips into given-up.

NIP-17 only queries the user's own DM relays (auth = self), so it rarely hits
this; it's the NIP-04 conversation fan-out to the correspondent's relays that
trips author-auth-required relays.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 20:24:23 +00:00
Claude ae62968160 fix: mark DM history exhausted only when every relay empty-EOSEs
Exhaustion was decided by "the round returned zero events," but a round can
return zero because relays CLOSED (auth) or never answered — not because they
reached the end. So "All caught up" appeared before slow/auth relays had
actually finished.

A relay is finished only when it returns an empty page followed by EOSE — the
pager already records exactly that in its per-relay `done` flag (CLOSED /
cannot-connect deliberately don't set it). So the chat is now exhausted only
when every relay is done (activeRelays is empty), per the rule "all relays must
return that EOSE." A post-auth empty EOSE that lands after the round already
settled on the earlier CLOSED now flips exhausted immediately too
(markExhaustedIfAllDone in onEose), not just at the next round boundary.

Because a chat is no longer "done" while a relay keeps CLOSING, the conversation
auto-fill (no stall-gate) would otherwise re-issue identical rounds and hammer
that relay. A no-progress guard skips re-issuing a round whose relay set and
zero-event result are unchanged; the pool re-auths on the open subscription and
its EOSE clears the guard and finishes the relay. All the new single-valued
state resets on account/conversation switch alongside the existing display flows.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 20:00:31 +00:00
Claude baad77a966 refactor: key the conversation history pager by a (account, ChatroomKey) type
Replaces the "pubkey/listId" string key with a small ConvoKey data class holding
the account pubkey and the ChatroomKey. ChatroomKey is a data class over the
participant set, so it's a collision-free key — unlike listId, which is its
32-bit hashCode as a string and can collide. Including the account keeps the two
accounts' views of the same correspondent on separate cursors (the manager is a
singleton shared across logged-in accounts).

Also lighter on allocation than the string it replaces: the per-relay-event hot
path captures the key once per subscription (no per-event construction), and the
remaining call sites build one small object instead of concatenating + hashing a
string.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 19:18:38 +00:00
Claude 0f0644200d fix: don't leak DM history state across logged-in accounts
The DM managers live in a single shared coordinator (Amethyst.instance.sources),
so every logged-in account uses the same instances. The per-account paging state
(pager cursors, started set, accounts map) was keyed by pubkey and fine, but the
single display flows — exhausted, relayCount, reachedBack, autoFillRoomMark —
were not. Switching from an account that had exhausted its history to another
left exhausted=true, so the second account's auto-fill was gated shut and its
chats never paged in.

Each history manager now tracks the active account/conversation and repoints its
display flows on switch: exhausted is restored per-account (kept in a small
exhaustedByUser/exhaustedByList map so an already-finished account shows "all
caught up" rather than re-paging), and the cosmetic flows + stall mark reset.
Paging cursors stay in the per-account pager, so progress is preserved.

Also scopes the conversation history pager key by account pubkey: a ChatroomKey
(hence listId) is identical for the same correspondent across accounts, so two
logged-in users viewing the same person would otherwise share one cursor.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 19:11:40 +00:00
Claude 0d0857c2e9 feat: modern DM history status card with "all caught up" finish
Replaces the bare spinner + "Load entire history" link with a status card that
tells the user what the app is actually reaching for: per protocol, it shows
"Older <encrypted|legacy> messages" with a subtitle of "<NIP-17|NIP-04> · N
relays · back to <month>" while it pages. When that protocol runs dry the card
doesn't just vanish — it crossfades to "All caught up · Reached the start of
your <…> messages", holds for a beat, then collapses away.

The history managers now surface the live status the card needs: relayCount
(relays the current page is asking) and reachedBack (oldest point paged to, from
the deepest per-relay cursor), added to all three history managers and computed
via UntilLimitPager.deepestUntil. The "load entire history" action is dropped —
scroll-driven paging already walks to exhaustion, so the link was redundant.

Each protocol's card sits at its own oldest-loaded boundary (rooms list) or both
stack at the conversation's oldest end, so the two protocols' loading is shown
independently at their real depths. New strings use a <plurals> for the relay
count.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 18:38:49 +00:00
Claude 5e67fe913f fix: page NIP-17 and NIP-04 history independently in the rooms list
The rooms list had a single auto-fill trigger and a single loading boundary,
both pinned to the oldest private room of EITHER protocol. When NIP-04 history
ran far deeper than NIP-17 (e.g. NIP-04 back to 2023, NIP-17 shallow), that
oldest room was a 2023 NIP-04 row at the very bottom, so gift-wrap loadMore only
fired when the user scrolled all the way down to it — NIP-17 never paged on the
way, and the loading indicator was only visible at the bottom.

Split the trigger and the boundary per protocol. Each protocol now widens on its
OWN oldest-loaded room (gated only on its own loader and its own room-count
stall-gate) and shows its OWN loading indicator at its own depth, so NIP-17 and
NIP-04 page independently as the user scrolls — matching their very different
histories. WidenPrivateWindowWhen is generalized to a per-protocol WidenHistoryWhen
called once per protocol (and once per protocol for the empty-feed hunt). Each
history manager carries its own auto-fill stall mark (autoFillRoomMark).

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-02 17:57:56 +00: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
Vitor PamplonaandGitHub a4aff84897 Merge pull request #3124 from nrobi144/feat/desktop-feed-ui-refresh
feat(desktop): Feed UI refresh — inline expansion, comments, related content
2026-06-02 08:02:50 -04:00
Vitor PamplonaandGitHub 3de0fdc4c5 Merge pull request #3122 from davotoula/feat/share-as-dm
Share content directly to a DM ("Send as DM" share target)
2026-06-02 08:00:35 -04: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