Two additional gates on top of the AI-companion guide:
- Automated tests for new logic — quartz/ and commons/ get unit
tests; bug fixes get a regression test; UI-only stays exempt
from automated UI tests but keeps the manual on-device test
plan + screenshots. Pointer into CONTRIBUTING.md § Interop tests.
- Code review pass before opening the PR — run a second-agent
review with a different model or skill (/simplify, /kotlin-review,
/security-review, /code-review), then re-run tests and manual
test plan to catch fix-introduced regressions.
CONTRIBUTING.md pointer paragraph updated to enumerate the two new
gates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to CONTRIBUTING.md targeting external PRs whose diff was
substantially authored by an AI coding assistant. Adds five gates the
existing doc does not cover:
- Research before code (issue still valid, decentralisation fit)
- Build and install both flavours (Play + F-Droid)
- Performance and resource hygiene (main-thread, coroutines,
LocalCache reuse, relay subscriptions, KMP source sets, lambda Log)
- Regression test plan alongside feature test plan
- Don't-touch list (signer/KeyStore, release pipeline, NIP direction)
Filename deliberately avoids AGENTS.md to keep internal dev-team
agents unaffected by auto-load conventions. CONTRIBUTING.md and the
PR template each gain a one-line pointer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add missing `## Commits` header so the ToC anchor resolves.
- Drop "Modules touched" and "Risk / rollback" from the PR description
checklist so CONTRIBUTING matches the PR template (PR template lost
those sections in 8ea7d1e).
- Fold "Human and AI contributions" into "Proof of testing" — the two
sections restated the same author-of-record rule.
- Remove duplicate "Sending issues over Nostr" subsection (already
covered under "Ways to contribute").
- Compress "Project layout" ascii tree into a bullet list and merge
with "Where code belongs".
- Tighten "Coding standards" (drop bullets that restated generic
don't-over-engineer guidance) and "Bounties" (single paragraph
instead of a quote block plus six bullets).
- Trim the interop "Running them" subsection to the non-obvious bits
(opt-in flags, run-matrix.sh not concurrency-safe) instead of
repeating paths from the table.
Net: 406 → 316 lines, no information lost.
- chore(cast): drop protocol-toggle settings, strings, perms, deps
- gate cast on play flavor via BuildConfig.IS_CASTING_AVAILABLE
- fix(cast): keep local player paused across recompose; disable transport while casting
fix(cast): handle every Chromecast session terminal state and HLS mime hint
fix(cast): repair DLNA Res ctor and add play-only protocol toggle
fix(cast): backfill Cast button for accounts with pre-existing video settings
fix(cast): include cast glyphs in symbol font subset and add diagnostic logs
fix(cast): wire jUPnP's required Jetty deps and survive dialog dismissal
fix(cast): add jetty-client so jUPnP can issue UPnP control requests
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.