quinn is now treated as a flawless-required interop target alongside
aioquic, picoquic, and quic-go. Most Rust-based Nostr/MoQ relays our
users run their servers on are built on quinn, so an interop regression
there is a user-visible regression.
Validated by a 3-round flakiness sweep (1 full matrix + 2 audio-critical
subsets) — zero result flakiness across 528 test executions across all
four peers, with two environmental docker-compose stall classes documented
separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an in-process local relay to Amethyst Desktop that persists all
received events to a per-account SQLite database using quartz's existing
EventStore infrastructure. On startup, the local store hydrates
DesktopLocalCache for instant feed rendering before remote relays connect.
- LocalRelayStore: manages per-account EventStore lifecycle, batched
write-through via BasicBundledInsert (250ms window), startup hydration
(contact list -> metadata -> recent content events)
- LocalRelayMaintenance: periodic cleanup (NIP-40 expiration, 30-day
prune, weekly VACUUM), disk space monitoring
- Settings UI: integrated into RelaySettingsScreen with statistics,
storage management (prune/vacuum/clear), JSONL export/import, and
error display
- OfflineBanner: animated banner in both SinglePaneLayout and
DeckColumnContainer showing offline status with local cache indicator
- Thread-safe store access via @Volatile + synchronized lock
- Skips re-enqueue during hydration (checks LOCAL_RELAY_URL)
- DB stored at ~/.amethyst/accounts/<pubkey8>/events.db
- Corrupt DB auto-detected and recreated on open
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cleaner conceptual model: the parsed state object is the place where
all derived facts about the content live, so `isMarkdown` becomes a
field on `RichTextViewerState` (populated by `RichTextParser.parseText`
using the same cheap heuristic).
`RichTextViewer` now calls `CachedRichTextParser.parseText` once at the
top and dispatches on `state.isMarkdown` instead of running a separate
scan. `CachedRichTextParser.isMarkdown(content)` and its dedicated
`isMarkdownCache` go away — the single `richTextCache` carries the
decision alongside the parsed segments.
Inner callers (`DisplaySecretEmoji`, `MultiSetCompose` reaction
preview, `DisplayUncitedHashtags`) are unaffected: they continue to
receive a fully-parsed state with all segments populated.
The markdown check is purely a function of `content`, but it was a
top-level function in RichTextViewer.kt — every distinct instance of
the same content (e.g. a quoted note rendered inside its quoter,
or the same viral note shown multiple places in a feed) re-scanned
the string.
Move the decision into `CachedRichTextParser` with its own small
LRU keyed on content.hashCode(). Composables still wrap the call in
`remember(content)` so recompositions skip even the LRU lookup.
`RelayBadges` runs once per note in complete-UI mode. The closed view
was opening three separate `relays.stateFlow` subscriptions (one per
icon slot, each with its own `mapNotNull`), and both the closed and
expanded views collected the relay list raw — every relay arrival on
a fanned-out note triggered an immediate recomposition.
Collapse to a single subscription that emits `relays.take(3)`,
throttled with `sample(500)` and `distinctUntilChanged`. Slot widgets
now pull from the resulting list instead of each owning a flow.
Same treatment for the expanded `RenderAllRelayList` (now sampled)
and `ShouldShowExpandButton` (wraps the cached
`createMustShowExpandButtonFlows` lookup in `remember(note)` so the
LRU is hit once per note instead of per recomposition).
Three targeted fixes for the jitter that shows up as soon as a feed
contains nested NoteCompose (reposts, quotes, BechLink previews):
* `calculateBackgroundColor`: only schedule the 5s "new item" fade
LaunchedEffect when the item is actually new and tracks read state.
Inner notes pass `routeForLastRead = null` and were parking a
coroutine for 5s per item, every scroll. Also drop a per-call
`Color.copy(alpha = 0f)` allocation in favor of `Color.Transparent`.
* `EventObservers` + `WatchBlockAndReport`: wrap the `StateFlow`
lookups in `remember(note)` so each recomposition of the same note
doesn't re-resolve `note.flow().…stateFlow` (and, in
`WatchBlockAndReport`, hit the synchronized LRU lookup) on every
pass. Affects observeNote / Replies / Reactions / Zaps / Reposts /
Ots / Edits and the per-item hidden-flow check.
* `produceCachedState` / `produceCachedStateAsync`: short-circuit on
cache hits. The previous `produceState` body always launched a
coroutine even when the LRU already had the value; this is the
common path for BechLink previews and draft notes during scroll.
Now we read the cache synchronously inside `remember`, and only
launch a `LaunchedEffect` for the actual miss.
No behavior change.
refactor(discover): restore exhaustive when in DiscoverTab.toTabIndex
refactor(discover): collapse DiscoverTab.toTabIndex to ordinal and drop redundant pager guards
processBitmap silently swallowed any exception from bitmap.toBlurhash()
or bitmap.toThumbhash() via runCatching{...}.getOrNull(), making it
impossible to diagnose why a video upload's published imeta had blurhash
but no thumbhash (or neither). Add an .onFailure { Log.w(...) } to each
runCatching so the actual exception class + stack reach logcat.
When the user long-pressed a playing video and tapped "Add Media to
Gallery", the share-action overlay (RenderTopButtons.kt:302) hard-coded
blurhash/dim/hash to null and built an inline MediaUrlVideo without
those fields. Result: the published ProfileGalleryEntryEvent (kind 1163)
carried only the URL, alt, e, m, and client tags — no x/dim/blurhash/
thumbhash — even when the source kind:1 imeta carried them.