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
Adds a cache-backed Cashu mint directory sibling to LocalCache.relayHints
that aggregates mint URLs from every relevant event the cache sees, and
wires it into the AddCashuWallet mint-URL text field as inline
autocomplete so users don't have to remember mint URLs.
What feeds the directory:
- NutzapInfoEvent (kind:10019) — every nostr user with a Cashu wallet
publishes their accepted mints there. A typical inbox of cached
profiles seeds a useful starter directory automatically.
- MintRecommendationEvent (kind:38000) — explicit public vouches.
- CashuMintEvent (kind:38172) — formal mint announcements from the
NIP-87 directory subscription.
How it's populated:
- LocalCache.updateMintIndex(event) is called from
justConsumeAndUpdateIndexes alongside updateHintIndexes, so every new
event with a mint URL adds to the index. wasNew gating prevents
re-emissions from inflating popularity counters.
- LocalCache.ensureMintDirectoryBackfilled() does a one-shot scan of the
existing notes + addressables maps. The autocomplete UI kicks this in
a LaunchedEffect on screen open so suggestions are useful before the
next relay round-trip.
Where it surfaces today:
- AddCashuWalletScreen — under the mint-URL OutlinedTextField, a
MintSuggestionList card shows up to 6 cache-derived suggestions ranked
by popularity desc + URL asc. Tapping a row fills the field (does not
auto-add — users typically want to Verify first). Filters out URLs the
user already added and exact matches of what they typed.
The MintPicker dropdown inside the Receive / Send dialogs is unchanged
— those only need to choose between mints the user already has in their
wallet, so no directory autocomplete applies there.
Tests: 8 unit tests cover normalisation (case-insensitive, trailing-slash
stripping, http(s) gating), popularity ranking, substring filtering,
limit enforcement, and malformed-URL handling.
URL normalisation: trimmed, lower-cased, trailing `/` stripped, scheme
must be http(s). Same URL with different casing or trailing slash
collapses to one entry so popularity counts correctly.
Implementation notes:
- MintDirectoryIndex lives in commons/jvmAndroid (uses ConcurrentHashMap;
iOS doesn't ship Cashu wallet yet).
- Thread-safe; safe to read from any dispatcher.
- No persistence — purely in-memory, accumulates over the session.
- Entries are never removed: stale entries don't hurt (user always
verifies before adding), and tracking which event added which URL
would add bookkeeping without UX benefit.
https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
Address bugs and gaps surfaced by an audit of the prior 14 commits.
JVM tests passed because of typealias / platform-type lenience that
won't hold on Native; these are real iOS compile / behavior issues.
BUG fixes (iOS compile failures):
- commons/.../Note.kt:899 — Iterable.sumOf { -> BigDecimal } is a
JVM-stdlib-only overload. Common stdlib ships sumOf only for
Int/Long/Double/Float/UInt/ULong. Replaced with fold(BigDecimal(0)).
- commons/.../Note.kt:889 — BigDecimal(it.event?.content): the quartz
expect-class constructor takes String non-null; JVM accepted nullable
via platform-type lenience and threw NPE caught downstream. Switched
to ?.let { content -> BigDecimal(content) }.
- commons/.../Note.kt:838 — `catch (e: java.lang.Exception)` -> `Exception`.
- commons/.../feeds/custom/FeedDefinitionBuilder.kt + FeedBuilderState.kt:
inline FQN `java.util.UUID.randomUUID().toString()` -> kotlin.uuid.Uuid.
random().toString() (Kotlin 2.0+, @OptIn ExperimentalUuidApi).
inline `System.currentTimeMillis() / 1000` -> TimeUtils.now() (already
used elsewhere in the codebase).
- commons/.../viewmodels/NestViewModelTest.kt: moved from commonTest to
jvmTest. The test imports NestViewModel + nestsclient, both of which
the prior PR moved to jvmAndroid. commonTest depends on commonMain
only, so the test would fail to compile for iosSimulatorArm64Test.
SUBTLE fixes:
- commons/.../UserRelaysCache.kt: the flow field used double-checked
locking on a non-volatile var. JMM hazard on Native (ARM weak memory
model) — outer fast-path could observe a partially-published
WeakReference. Added @kotlin.concurrent.Volatile.
- commons/.../util/UrlValidation.ios.kt: NSURL.URLWithString("http:")
returns non-null with scheme="http" and no host; JVM's URI.toURL()
rejects with MalformedURLException. Reject scheme-only network URLs
(http/https/ws/wss/ftp without a host) to match JVM behavior.
- commons/.../util/KmpLock.kt commonMain doc: corrected "NSLock" ->
"NSRecursiveLock" to match the actual iOS implementation.
verifyKmpPurity gate extended (commons + quartz):
- Adds patterns: System.currentTimeMillis, Thread.sleep, java.util.UUID,
kotlin.jvm.Synchronized, kotlin.jvm.Volatile.
- Each pattern paired with a hint pointing at the canonical KMP
replacement; the error message surfaces both.
- Skips lines that start with //, *, or /* to avoid false positives on
KDoc / migration notes.
DAL extraction (commons/src/jvmAndroid/.../model/nip52Calendar/):
- CalendarSortKeys, CalendarAppointmentView, MonthGridBars, IcsExport now
live in commons so desktop and the future CLI can consume them. Package
changed to com.vitorpamplona.amethyst.commons.model.nip52Calendar.
- IcsExportTest moved to commons/jvmTest so it can see the `internal`
escapeText helper without exposing it as public API.
- Feed-filter classes (CalendarAppointmentsFeedFilter,
CalendarCollectionsFeedFilter) stay in amethyst — they depend on Account
and LocalCache. The ViewModels stay for the same reason; lifting them
requires moving Account/LocalCache too, out of scope here.
- A few smart-cast call sites needed local bindings because cross-module
properties don't support implicit smart-cast.
Accessibility:
- Each month-grid cell announces a full content description ("Wednesday,
January 15, 2025, 2 events, today, selected") via mergeDescendants so
TalkBack reads the cell as one item with role=Button.
- Week-strip cells get the same treatment with role=Tab.
- Header title now announces both the title and "jump to today" so the
affordance is discoverable.
- The expanding FAB describes itself as a toggle, and each sub-FAB as
the concrete create action.
Inline-FQN cleanup pass:
- CalendarEventDetailScreen (already in a prior pass), CalendarCollectionsView,
NewCalendarEventScreen, NewCalendarCollectionScreen: hoisted
fully-qualified androidx.compose / com.vitorpamplona references into
proper imports per the codebase style.
All 56 calendar tests still pass (48 in amethyst + 8 IcsExport in commons).