Follow-up to the previous geode rename — quartz's jvmAndroid tests
extend the geode testFixtures `RelayClientTest` and were calling
`defaultRelay.preload(...)`. After moving `preload` off `RelayEngine`
and into an extension function in `geode.testing`, these tests need
the import explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames in the geode module to make naming honest:
- Relay -> RelayEngine (the transport-agnostic core)
- LocalRelayServer -> KtorRelay (Ktor/CIO transport; defaults to 127.0.0.1
but supports public deployment, so "Local" was misleading)
- RelayHub -> InProcessRelays (URL-keyed registry that doubles as a
WebsocketBuilder for in-JVM tests; "Hub"/"Pool" implied substitutable
resources, but each URL maps to a distinct relay)
Also moves the test-only helpers `preload` and `publish` out of
RelayEngine and into a testFixtures extension file. As part of that,
`publish` now waits for the relay's OK reply before returning so that
publish-then-subscribe is deterministic — previously the fire-and-forget
submit let a subscription register after publish returned but before the
ingest queue fanned out, causing ephemeral kinds (not persisted) to leak
to late subscribers. Fixes the flaky
`Nip01ComplianceTest.ephemeralEventIsNotStoredAndDoesNotShowOnFollowupReq`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-review cleanup of the previous fix commit. No behavior change:
1. Removed nullability from the `nestViewModel` parameter — the only
call site always passes it, and the previous chain
`nestViewModel?.uiState?.collectAsState()?.value` was a
conditional composable that only worked by accident.
2. Replaced the hand-rolled `targetCanSpeakByRole` with
`ParticipantTag.canSpeak()` so future role additions stay
consistent.
3. Split the monolithic sheet into ParticipantSheetHeader,
AudienceActions, HostActions, ParticipantZapDialog and
DestructiveConfirmDialog so each piece recomposes on its own
state and has a single responsibility.
4. Factored the `broadcast(...) + toast + onDismiss()` triplet into
a `hostAction(...)` helper inside HostActions and a top-level
`broadcast(...)` utility for the destructive dialogs.
5. Collapsed the 13 pre-resolved toast strings into a
`ParticipantToasts` data class produced once by
`rememberParticipantToasts(displayName)`.
6. Made displayName reactive via `observeUserInfo` so the header
and toast text always agree (previously the toast used a
snapshot while the header observed the metadata flow).
7. Replaced the deeply qualified types in the signature with
normal imports.
8. Consolidated three `rememberSaveable<Boolean>` dialog flags
into a single `SheetDialog` enum with a name-based Saver, so
the dialog states are mutually exclusive by construction.
https://claude.ai/code/session_013Aj1h3epWkvucKFLdunjcv
A heavily-edited note can fire `edits.stateFlow` hundreds of times
during initial relay sync; without throttling, each emission still
hits the IO scan even though `distinctUntilChanged` would collapse
most of them downstream. `sample(500)` keeps only the most recent
state per ~half second, so the IO `findLatestModificationForNote`
runs at most ~twice a second per note instead of once per arrival.
- Gate the new notificationsFollowing / notificationsEveryone fan-out in
updateFeedsWith / deleteNotes on splitNotificationsEnabled.value so the
default-off case stops doing two extra filter scans per event bundle.
- Lazy-init NotificationFeedFilter.overrideFollowLists so the
SharingStarted.Eagerly topNavFilter pipeline only starts when the
split UI actually opens the filter.
- Switch SplitNotificationsScaffold to rememberForeverPagerState so the
active tab survives recomposition, matching HomeScreen/DiscoverScreen.
- Promote the duplicated "Notification" last-read key string to a
NOTIFICATION_LAST_READ_KEY const.
- Move the opaque-background fix into SummaryBar itself so callers don't
need a band-aid Modifier.background on their topBar Column.
- Trim narrating / WHAT comments; keep only the deep-link scroll WHY.
Adds an opt-in toggle under Settings → Notifications that splits the
notifications screen into two pinned-mode tabs — Following (kind:3
follow list only) and Everyone (global). The bottom-bar unread dot
glows only for Following so it behaves like the DM-style indicator
the bounty asks for. Default is off; existing single-feed behavior
and the FeedFilterSpinner are unchanged when the toggle is off.
Fixes ten issues found while reviewing the per-participant ModalBottomSheet
on the Nest full-screen UI:
1. demoteToListener used to fall through to setRole(PARTICIPANT) when the
target had no participant row, silently adding a pure-audience listener
as PARTICIPANT to the kind-30312. Now returns null in that case and
when the target is already PARTICIPANT.
2. Kick and Force-mute now require an AlertDialog confirmation before
firing, matching the host's own Leave-room flow.
3. Sheet header switched from a raw hex stub to UsernameDisplay so the
user knows who they're acting on.
4. Local Hush is hidden when the target isn't currently broadcasting —
the volume gate attaches to an active subscription, so a hush against
a non-speaker was a silent no-op.
5. Follow/Mute/Promote/Demote/Force-mute/Kick now emit success toasts
(launchSigner already surfaces errors).
6. isFollowing and isHidden moved to collectAsState so labels update
live if the kind-3 / mute-list publish lands while the sheet is open.
7. Force-mute row carries a "May be ignored by clients that don't honor
the verb" subtitle (the verb is an Amethyst extension over nostrnests).
8. Tap on a stage avatar now opens the same sheet as long-press — the
audience grid already worked this way; the stage was long-press only.
9. Hand-raise queue avatars are now click+long-press → sheet, so a host
can profile / kick a raised-hand audience member without switching tabs.
10. Promote-to-Speaker / Promote-to-Moderator / Demote-to-Listener rows
are hidden when the target already has that role.
https://claude.ai/code/session_013Aj1h3epWkvucKFLdunjcv
`observeEdits` in NoteCompose previously listened to the raw
`note.flow().edits.stateFlow` and re-ran `findModificationEventsForNote`
(an IO scan) inside a `LaunchedEffect` on every emission — even when
the emission didn't change the actual list of modifications. That
mutated `editState` repeatedly with the same value during scroll on
any active TextNote.
Add `observeNoteModifications` in EventObservers.kt: it does the IO
resolution via `mapLatest { LocalCache.findLatestModificationForNote(note) }`
on Dispatchers.IO, then `distinctUntilChanged()` — so the State only
updates (and the consumer's LaunchedEffect only re-keys) when the
modification list truly changes. The State is `null` until the first
IO resolution completes; consumers treat that as "still loading" and
don't flip the UI to "no edits" prematurely.
Drop the now-unused `observeNoteEdits` and the
`AccountViewModel.findModificationEventsForNote` wrapper.
`TimeAgo`, `NormalTimeAgo`, `ChatTimeAgo`, and the chatroom header's
private `TimeAgo` previously formatted the timestamp once inside
`remember(time)` and never refreshed — a note shown when it was 59 s
old would keep saying "59s" forever, even when it had been minutes.
Introduce a single app-wide ticker:
* `LocalNowSeconds` is a `CompositionLocal<State<Long>>` whose value
is refreshed every 30 s by a single `produceState` coroutine inside
`NowProvider` (mounted once at the app root in `MainActivity`).
* Each `TimeAgo` composable reads the ticker inside `derivedStateOf`,
so it only triggers a Text recomposition when the formatted string
actually crosses a threshold (e.g. 1m → 2m). Ticks that don't change
the displayed string are filtered by `derivedStateOf` equality.
One coroutine total, no per-item timers, and recompositions are
proportional to "strings that actually need to update" rather than
"items on screen × ticks/second".
Also drop DLNA from the cast description (functionality removed) in English and Polish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the raw viewModelScope.launch + runCatching with launchSigner,
matching the rest of the codebase. Same survival-past-dismiss
guarantee, plus signer errors (read-only key, NIP-46 unauthorized /
timeout, signer not found) now surface as toasts instead of being
silently swallowed.
The HomeTabsSettingsScreen toggles (New Threads / Conversations /
Everything) wrote to UiSettingsFlow but UiSharedPreferences had no
DataStore keys for them, so the debounced save was a no-op and startup
always reloaded the defaults. Add the missing keys, reads, and writes.
The stage/audience LazyVerticalGrid uses Adaptive(80dp) cells while
the avatar wants 75dp. Wrapping the avatar in an inner Box with
`Modifier.padding(ringPadding=12dp)` reserved room for the speaking
glow + outer ring, but it also reduced the avatar's max **width** to
56dp on a typical 82dp cell while leaving its height at the requested
75dp — Compose grid items are tight on width and free on height.
Result: a 56×75 ellipse instead of a circle.
Drop the inner padding and size the outer drawBehind Box explicitly
to `avatarSize + ringPadding * 2`. AvatarAndBadges still sizes tight
to the picture (so role/hand/mic/reaction badge corner alignment is
unchanged), but no longer inherits a width-only constraint from the
cell — the 75dp picture stays a circle.
The per-participant host actions (Promote to Speaker, Promote to
Moderator, Demote, Force Mute, Kick) and the Hand-Raise queue's
Approve button all launched their sign+broadcast on a
rememberCoroutineScope() bound to the local composable. Because
every action row calls onDismiss() (and the hand-raise row may
disappear when canSpeak() flips true), the composable leaves the
tree synchronously after the click, cancelling the scope before
the launched coroutine ever runs sign(template). The user saw the
sheet close and nothing else — the kind-30312 republish / kind-4312
admin event was never actually emitted.
Launch on accountViewModel.viewModelScope instead so the
broadcast outlives the dismissing UI.
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>