Commit Graph

12870 Commits

Author SHA1 Message Date
Vitor Pamplona 12ac22b53b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-28 18:08:51 -04:00
Vitor Pamplona a538138b25 Merge pull request #2633 from vitorpamplona/claude/fix-amethyst-nest-reconnecting-T8DSP
Update MOQ relay URLs to include port 4443
2026-04-28 18:04:36 -04:00
Claude 882a7b5501 Revert "fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial"
This reverts commit 23fab05c8a.
2026-04-28 21:56:30 +00:00
Vitor Pamplona 356ddffa99 Merge pull request #2630 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 17:55:35 -04:00
Vitor Pamplona de773d06f9 Fixes test cases 2026-04-28 17:54:29 -04:00
Crowdin Bot 3fd30cc820 New Crowdin translations by GitHub Action 2026-04-28 21:54:06 +00:00
Vitor Pamplona b81bebf6bb Merge pull request #2632 from vitorpamplona/claude/fix-gesture-nav-spacing-BC4nv
Fix window insets handling in NestActionBar
2026-04-28 17:52:24 -04:00
Claude 045bc36371 fix(nests): respect navigation bar insets in NestActionBar
The bottom action bar in NestFullScreen was rendering under Android's
gesture navigation area in edge-to-edge mode, leaving the leave / talk
/ react buttons partially obscured. Apply BottomAppBarDefaults
windowInsets to the surface so it pads above the system navigation.
2026-04-28 20:59:22 +00:00
Vitor Pamplona dcc6a7e07d Merge pull request #2631 from vitorpamplona/claude/wire-jni-secp256k1-kYhKZ
Update schnorr256k1-kmp dependency to v1.0.3 and fix group ID
2026-04-28 16:28:52 -04:00
Claude 23fab05c8a fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial
The Android emulator (and a number of dual-stack networks where the
IPv6 path is broken) advertises working v6 connectivity but silently
drops outbound packets. With the JDK / RFC 6724 default of AAAA-first,
any host that publishes a stale or unreachable AAAA record — e.g.
moq.nostrnests.com, whose Linode v6 currently doesn't accept
connections — causes every HTTPS call and every QUIC handshake to
fast-fail with ConnectException, parking the Nests room screen on
"Reconnecting" indefinitely.

Plug a small Dns wrapper into both OkHttp factories that puts
Inet4Address entries ahead of v6 in the resolved list (v6 stays as
fallback for genuinely v6-only hosts), and switch UdpSocket.connect
from getByName to getAllByName + IPv4-first selection so the QUIC
audio path makes the same choice as the HTTPS auth POST.
2026-04-28 20:26:49 +00:00
Claude ecb985e2b4 build(quartz): bump schnorr256k1-kmp to 1.0.3 with bundled JVM JNI
Pulls in the per-platform JNI shards (linux-x86_64, linux-aarch64,
darwin-x86_64, darwin-aarch64) that the upstream library now publishes as
runtime dependencies of schnorr256k1-kmp-jvm. The 1.0.3 release also
moved the groupId to com.vitorpamplona.schnorr256k1.

Net effect: Secp256k1CrossValidationTest's customCImplementationMatchesAcinqAndKotlin
no longer prints SKIP, and the Custom C(JNI) column in
Secp256k1TripleBenchmark now reports numbers (verifySchnorr ~25.6k ops/s,
signSchnorr cached pk ~57.4k ops/s) instead of (N/A). 188/188 tests pass
in the secp256k1 suite on a stock JVM with no LD_LIBRARY_PATH or
-Djava.library.path needed.

https://claude.ai/code/session_01XEfLBStDum7h8Brwo7qFXt
2026-04-28 20:20:28 +00:00
Vitor Pamplona 17e04fc692 Centralizes the empty mesaage. 2026-04-28 16:18:24 -04:00
Claude 412b2a285c fix(nests): point default moq.nostrnests.com URLs at port 4443
The public nostrnests deployment serves moq-auth + moq-relay on
:4443; :443 is not the live endpoint. With the old defaults a
freshly-created room could never mint a JWT, so the room screen
parked on "Reconnecting" forever and the room never showed up in
the nostrnests UI.
2026-04-28 20:10:35 +00:00
Vitor Pamplona 629915dc6c Merge pull request #2628 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 15:58:45 -04:00
Vitor Pamplona 7818fd5db3 Merge pull request #2629 from vitorpamplona/claude/expandable-nestfullscreen-summary-H38Ey
Refactor nest room UI: collapsible summary and stage header redesign
2026-04-28 15:58:31 -04:00
Claude 7fe88ac4be Collapse Nest summary by default; tap title to expand
Move the LIVE chip and listener count out of the header strip and into
the right side of the Stage card's title row (listeners first, then
chip). The remaining summary now hides by default and toggles when the
user taps the room title in the top bar, reclaiming vertical space for
the stage and chat.
2026-04-28 19:55:40 +00:00
Crowdin Bot c99ad72794 New Crowdin translations by GitHub Action 2026-04-28 19:52:19 +00:00
Vitor Pamplona 0f35777bd6 Better size for the Admin badge on Nests 2026-04-28 15:35:05 -04:00
Vitor Pamplona 24336cb940 Fixes scroll to newest when a new message arrives 2026-04-28 15:19:08 -04:00
Vitor Pamplona ee860b92a1 Adjusts the roudabout way of making the chat screen 2026-04-28 15:11:17 -04:00
Vitor Pamplona 2a7683a7c2 Smaller Avatars on Nest 2026-04-28 15:10:54 -04:00
Vitor Pamplona 17054fc35a Links the Tor okhttpclient with nests 2026-04-28 14:45:14 -04:00
Vitor Pamplona 66557268f7 only leaves if the event changes while watching. 2026-04-28 14:44:43 -04:00
Vitor Pamplona 98e62b167b Merge pull request #2621 from vitorpamplona/claude/review-nostr-nests-compliance-hKBnS
feat(nests): proactive JWT refresh + reconnect for speaker path
2026-04-28 13:56:06 -04:00
Claude cee9d8a430 feat(nests): retry 429 with backoff + re-sign NIP-98 on each retry
Lets a clustered burst of mint calls (typical interop test scenario,
also any moq-auth deployment with stricter rate limits) outlast the
60 s rate-limit window instead of cascading into a hard failure.

- Retry up to 7 times on HTTP 429. Worst-case total wait at 1 s
  initial + 16 s cap is 1+2+4+8+16+16+16 = 63 s — just past the
  reference moq-auth 60 s/IP window.
- Respect the `Retry-After` header (delta-seconds OR HTTP-date)
  when present; fall back to capped exponential backoff otherwise.
- Re-sign the NIP-98 auth event on every attempt. The reference
  validator accepts a 60 s validity window; without re-signing,
  any retry that waits past that window fails 401 "Event too old".
- Suspending `delay` so coroutine cancellation tears the loop down
  cleanly.

`./gradlew :nestsClient:jvmTest -DnestsInterop=true -DnestsInteropExternal=true`
now goes green in a single invocation: 157 tests across 33 classes,
0 failures. Previously the same command cascaded 11 failures once
the moq-auth 20/min/IP bucket filled.

Unit tests: 7 new cases covering Retry-After parsing (seconds,
HTTP-date, garbage, missing), exponential cap, the 429-then-200
retry loop, max-retries exhaustion, and that non-429 4xx is NOT
retried. Backed by a tiny ServerSocket-based HTTP fake so no new
test deps.
2026-04-28 17:34:22 +00:00
Claude 461147af81 docs(nests): update plan doc for round-2 listener survival fixes
Captures the group-rotation, orchestrator-break-on-Closed removal,
ensureAnnounceWatchStarted, handleInboundBidi refactor, and
removeInboundSubscription changes that landed in d8ab4fd9. All
three reconnecting-listener interop scenarios now pass.
2026-04-28 17:23:10 +00:00
Claude d8ab4fd99b fix(nests): rotate moq-lite groups + reconnect after publisher cycle
To make a SubscribeHandle survive a publisher session swap on the same
relay, three things had to change together:

1. Broadcaster emits one Opus frame per moq-lite group
   (`publisher.send` + `publisher.endGroup`). moq-lite's "from-latest"
   subscribe semantics deliver a new subscriber the NEXT group's
   frames; without per-frame rotation a subscriber that attaches
   mid-broadcast waits forever for the (single, never-ending) group
   to end.
2. MoqLiteSession opens its announce-watch bidi synchronously before
   the first subscribe, dispatches subscribe + announce frames over a
   single long-running collector (varint type code hoisted outside
   `collect`), and FINs the publisher's currentGroup when an inbound
   subscribe bidi closes — so the next send opens a fresh group keyed
   off the live subscriber instead of the recycled one.
3. ReconnectingNestsListener no longer breaks on terminal=Closed in
   the orchestrator; the user-driven stop path goes through
   `orchestrator.cancel()`, so any other Closed (peer-driven
   transport close, half-broken session, publisher recycle) is a
   reconnect trigger.

Round-trip interop test updated to assert `groupId == idx` (one group
per frame) rather than `groupId == 0`. All three reconnecting-listener
interop scenarios now pass against the real moq-rs relay:
happy-path, session-swap, and listener-survives-publisher-recycle.
2026-04-28 17:06:09 +00:00
Claude 5f71dc7c76 test(nests): fix nextSubscribeBidi helper to terminate after match
The takeWhile/collect pattern only re-checks its predicate when the
NEXT upstream value emits, so once nextSubscribeBidi found its
target Subscribe bidi, the helper sat blocked waiting for a
follow-up bidi that may never come — turning groups_are_demuxed_by_
subscribeId into a 5-minute hang on tests that open exactly two
subscribes (the announce-watch bidi happened to land between, but
relying on it to nudge the flow forward is fragile).

Switched to transformWhile { … emit(…); false }.firstOrNull() so
the upstream collection terminates synchronously after the first
match. Warm-daemon test time drops from multi-minute timeout back
to the expected ~10 ms range.

https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
2026-04-28 16:22:01 +00:00
Claude cd9279d23b test(nests): skip housekeeping bidi in groups_are_demuxed_by_subscribeId
The session-layer fix in 851045c6 lazy-launches a single shared
announce-watch bidi on first subscribe (publisher-disconnect
detection). The unit test groups_are_demuxed_by_subscribeId
assumed each session.subscribe() opened exactly one peer-side bidi
and grabbed them by raw `peerOpenedBidiStreams().first()`, which
races with the announce-watch bidi.

New helper nextSubscribeBidi(serverSide) iterates accepted bidis,
peeks the control-byte varint, and skips any that aren't Subscribe.
The race window (announce-watch bidi between subscribe #1 and
subscribe #2) is now handled gracefully — the test reliably
finds both subscribe bidis regardless of the announce-watch's
launch ordering.

Also extends the listener-survives-publisher-recycle plan doc
with the full diagnosis of why
reconnecting_wrapper_keeps_handle_alive_across_session_swap
remains a separate, narrower failure (publisher single-group
architecture: NestMoqLiteBroadcaster never rotates groups, so
mid-stream listener resubscribes have nothing to attach to).

https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
2026-04-28 16:03:11 +00:00
Vitor Pamplona a22476f4b4 Merge pull request #2626 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 11:45:42 -04:00
Vitor Pamplona 594ead86fe Merge pull request #2627 from vitorpamplona/claude/review-libsecp-migration-Ql9hH
Remove custom C secp256k1 implementation, migrate to libschnorr256k1
2026-04-28 11:42:45 -04:00
Claude 2ad1a48123 refactor(quartz): migrate in-tree C secp256k1 to libschnorr256k1-kmp
The custom C secp256k1 implementation under quartz/src/main/c/ was never
wired into Gradle (no externalNativeBuild block) and only existed for the
3-way benchmark + cross-validation test against ACINQ on JVM/Android.
The C library has been split out to vitorpamplona/libschnorr256k1{,-kmp},
so the in-repo copy is dead weight.

This change replaces it with the published Maven Central artifact
`com.vitorpamplona:schnorr256k1-kmp:1.0.0`, scoped to test/benchmark
configurations only — production crypto continues to use ACINQ
secp256k1-kmp on JVM/Android and the pure-Kotlin Secp256k1 on native.

The Android AAR ships libschnorr256k1_jni.so for arm64-v8a and x86_64,
so the Android benchmark now exercises the C row automatically (no more
manual build_android.sh). On JVM the .so still has to be installed by the
developer; the cross-validation test and triple-benchmark gracefully skip
the C row when System.loadLibrary fails, matching prior behavior.

Verified on JVM: all 13 secp256k1 jvmTest classes pass (188 tests), and
ACINQ vs pure-Kotlin benchmark numbers match the documented baseline
within sandbox noise (verifySchnorr ~15k ops/s, signSchnorr cached
~33k ops/s, pubkeyCreate+Compress ~36k ops/s).

Removes ~5,100 LOC: quartz/src/main/c/ (4,500), Secp256k1InstanceC.*
expect/actual shim (560), Secp256k1C JNI declaration object.

https://claude.ai/code/session_01KnvpK2amcVZKfFiZJvHjVe
2026-04-28 15:36:25 +00:00
Crowdin Bot 3bfd49b94f New Crowdin translations by GitHub Action 2026-04-28 15:25:12 +00:00
Vitor Pamplona 090f7499ae Merge pull request #2624 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 11:23:44 -04:00
Vitor Pamplona b5c19f1320 Merge pull request #2625 from vitorpamplona/claude/review-pip-transition-ui-2UYbc
Enhance Nest PIP with dynamic speaker focus and connection status
2026-04-28 11:22:50 -04:00
Claude 4c63e58e68 feat(nests): PIP focuses on active speakers, surfaces self + connection state
Pulls three high-signal features from NestScreen into the PIP layout
without crowding the small floating window:

- Audio-level pulsing rings on active speakers, mirroring the
  full-screen StageGrid contract. The ring width tracks live audio
  level so the PIP visibly responds to who is talking, instead of
  showing static borders.
- Adaptive speaker selection: when anyone is speaking, PIP shows only
  the active speakers (up to 4); when exactly one person is talking
  the avatar bumps to 56dp for a clear focus mode. With no one
  speaking, falls back to the first 4 on-stage participants so the
  PIP isn't blank during silence. Fixes the 'same 4 faces while
  someone else talks off-screen' problem in big rooms.
- Self-status row (mic broadcasting / muted, hand-raised), so a
  glance at the PIP answers 'am I muted?' without expanding back.
  Hidden for pure audience listeners with no hand to keep clutter low.
- Reconnecting / Failed caption over a dimmed body so audio drops
  are obvious — previously the avatars stayed lit even with no audio
  arriving.

https://claude.ai/code/session_01BpwAxN9gs79CJuWKqf3BEB
2026-04-28 15:17:44 +00:00
Claude 851045c654 fix(nests): listener subscriptions survive publisher recycle
Two layered fixes for the gap captured in
nestsClient/plans/2026-04-28-listener-survives-publisher-recycle.md.

Session layer (MoqLiteSession.kt):
  - Lazy single shared announce-watch pump per session, opened on
    first subscribe. moq-lite Lite-03 has no explicit "publisher
    gone" message on the subscribe bidi (the relay keeps that
    bidi open across publisher cycles in case a fresh publisher
    takes over the suffix), so the announce stream's Ended event
    is the only reliable signal.
  - On Announce(Ended) for a broadcast suffix, close the
    matching ListenerSubscription's frames Channel and remove it
    from the map. The wrapper-level frames.consumeAsFlow() flow
    ends naturally — same shape as a user-driven
    handle.unsubscribe() — so the wrapper pump's collect-
    completion path drives the re-issue.

Wrapper layer (ReconnectingNestsListener.kt):
  - Inner re-subscribe `while (currentCoroutineContext().isActive)`
    loop in reissuingSubscribe. When the underlying frames flow
    completes (publisher cycled, signalled by the session layer
    above), re-issue subscribe against the same listener with a
    100 ms backoff. moq-lite supports subscribe-before-announce so
    a re-subscribe issued during the gap attaches cleanly when
    the next publisher comes up under the same suffix.

Verified against the real moq-rs relay (host build, external
mode): the new
NostrNestsReconnectingListenerInteropTest.subscribe_handle_survives_publisher_recycle
test passes — single SubscribeHandle keeps emitting frames across
multiple speaker JWT-refresh cycles. Speaker reconnect tests
still pass too.

In production, this closes the audio dropout that would have
fired every 9 minutes per speaker JWT refresh on long Nest calls
(see the plan doc for the original diagnosis).

https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
2026-04-28 15:06:07 +00:00
Claude 4756348221 feat(nests): back gesture and Minimize button enter PiP instead of leaving
The only way to keep audio playing while leaving the room screen was the
Home/Recents gesture (onUserLeaveHint). Back swiped the user out of the
session entirely, and there was no in-UI affordance signalling that PiP
existed. Users wanting to step away had no obvious path that preserved
audio.

- BackHandler in NestActivityContent: while connected and PIP is
  supported, back minimizes to PIP. Disconnected/Connecting/Failed
  fall through to the default finish() so cancelling still works.
- Minimize IconButton in the room TopAppBar (hidden when the device
  doesn't advertise FEATURE_PICTURE_IN_PICTURE).
- Public NestActivity.enterPip() consolidates the connected + feature
  guards so onUserLeaveHint, BackHandler, and the Minimize button all
  share one entry point.

https://claude.ai/code/session_01BpwAxN9gs79CJuWKqf3BEB
2026-04-28 15:03:34 +00:00
Crowdin Bot 33dccc1d7a New Crowdin translations by GitHub Action 2026-04-28 14:35:17 +00:00
Vitor Pamplona 30e34a408a Merge pull request #2623 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 10:33:15 -04:00
Vitor Pamplona db7932baac Merge pull request #2620 from vitorpamplona/claude/improve-participate-grid-FVS9L
feat(nests): bigger avatars + clearer mute/speaking state in participant grid
2026-04-28 10:32:56 -04:00
Claude a0c7889183 feat(nests): beauty pass on the participant grid
Seven coordinated visual changes so the grid feels less prototype-y
and more native to the user's Material 3 theme:

#1 Colour harmonisation. Drops the hardcoded Tailwind hexes for
   host (purple-500), moderator (sky-500) and hand-raise (yellow-500)
   in favour of MaterialTheme.colorScheme tones — host → primary,
   moderator → secondary, hand → tertiary, each paired with the
   matching onContainer for the icon tint. Speaking-green is kept as
   a hardcoded brand colour because "green = mic active" is a
   universal convention (Spaces, Clubhouse, Discord); a primary tint
   would lose that signal.

#2 Stage strip as a tonal card. StageGrid now wraps its label + grid
   in a Surface(color = surfaceContainerLow, shape = 20.dp rounded)
   so the active speakers visually live in their own zone, separated
   from the chat / audience tab below.

#3 Breathing room. GRID_SPACING goes from 6.dp to 8.dp — still keeps
   4 cells per row on a 411.dp Pixel (4*96 + 3*8 = 408 ≤ 411). The
   stage card adds 12.dp internal padding.

#4 Empty-stage hint with icon. Adds an HourglassEmpty glyph above
   the existing "Waiting for speakers…" copy so the empty state reads
   as intentional rather than as a layout glitch.

#5 Ring color crossfade. animateColorAsState on the speaking-ring
   color in addition to the existing animateDpAsState on width — the
   transition from idle → speaking → muted → idle no longer snaps.

#6 Soft outer halo. New drawBehind layer paints a transparent
   speaking-green circle scaled by the live audio level. Uses
   drawBehind so the glow extends beyond the avatar's layout bounds
   without affecting cell measurement; alpha animates to 0 when the
   speaker stops, so it dims rather than disappears.

#7 Reaction chips. SpeakerReactionOverlay wraps in AnimatedVisibility
   with fade + scale-in/out so a 👏 burst pops in and dims out
   instead of snapping. Chips swap secondaryContainer fill for a
   tonal Surface (tonalElevation 2dp + shadowElevation 1dp) — softer
   in light mode, properly tinted in dark mode.
2026-04-28 14:26:40 +00:00
Claude 1c8c961762 docs(nests): plan for listener-survives-publisher-recycle gap
Discovered while validating connectReconnectingNestsSpeaker against
the real moq-rs relay: when a publisher cycles its session (e.g. via
JWT refresh), an existing listener-side SubscribeHandle does not
auto-reattach to the new publisher under the same broadcast suffix.
Frames stop until the listener's own JWT refresh fires.

Root cause is multi-layer:
  - moq-lite session: SubscribeHandle.frames is a
    Channel.consumeAsFlow() that the session never closes on remote
    disconnect.
  - moq-lite Lite-03: graceful close emits Announce(Ended), which
    the wrapper would need to observe to react.
  - ReconnectingNestsListener: reissuingSubscribe only re-issues on
    listener-session swaps, not on announce-Ended.

A wrapper-layer announce-driven re-subscribe was attempted (race
collect vs announce-Ended.first(), cancel loser, unsubscribe, retry).
It compiled and the unit tests passed, but interop against the real
relay still showed listener silence after the recycle — the listener's
QUIC session terminated 4 ms after the publisher's "subscribe
cancelled" log line, suggesting the announce-bidi cleanup or
sub-unsubscribe path is being interpreted as session close at the
moq-lite layer. Did not pin down the exact chain.

Reverted the wrapper change to keep the branch clean. Speaker-side
connectReconnectingNestsSpeaker is unaffected and continues to pass
its interop tests. Plan doc captures the diagnosis and three
candidate fix paths for follow-up — preferred direction is
session-layer (have MoqLiteSession close the frames Channel when the
relay forwards Announce(Ended) or FINs the subscribe bidi) so the
existing wrapper pump's natural collect-completion handles it.

https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
2026-04-28 13:29:10 +00:00
Claude ae2bae13ac feat(nests): self highlight, floating reactions, kick drops p-tag, tap-self mute
#1 Local user's cell tints the username in colorScheme.primary so you
   can find yourself in a busy stage. Threads myPubkey through StageGrid
   and AudienceGrid; cells receive isSelf and pass MaterialTheme primary
   to UsernameDisplay's textColor.

#2 SpeakerReactionOverlay moves from below the username (which reflowed
   the column when reactions arrived and shoved neighbors down) to
   inside the avatar Box, anchored bottom-end with a small outward
   offset so it overlaps the corner without colliding with the
   bottom-center mic badge. Cell height stays constant whether the
   speaker has 0 or 5 active reactions.

#3 Kick now mirrors nostrnests' two-step path: the existing ephemeral
   kind-4312 ['action','kick'] kicks the target off the audio plane,
   followed by a re-published kind-30312 with their p-tag dropped so
   the participant grid stops rendering them. New
   RoomParticipantActions.removeParticipant builds the second event;
   refuses to drop the host (same protection as setRole demoting host).

#7 Tap-to-mute on your own avatar. Self-cell receives onTapSelf which,
   when broadcasting, toggles BroadcastUiState.Broadcasting.isMuted via
   the existing setMicMuted path. Falls back to no-op when not
   broadcasting so a non-broadcasting tap doesn't surface a useless
   action — the avatar simply isn't tappable.
2026-04-28 13:21:21 +00:00
Claude 52a506dd92 test(nests): scope speaker-refresh interop to speaker invariants only
The first cut of the JWT-refresh interop test held a single listener-
side SubscribeHandle open across the speaker's session recycle and
asserted both pre- and post-recycle frames arrived on it. That fails
because moq-lite's relay doesn't auto-route a vanilla SubscribeHandle
to a fresh publisher session under the same suffix — the
listener-survival-across-publisher-recycle is a separate concern,
NOT a speaker-reconnect bug. Verified against a real moq-rs relay
(host-built, external mode, --auth-key-dir + per-kid JWK file).

Reworked to validate just what the speaker reconnect should
guarantee:
  - Phase 1: pre-refresh frames round-trip on the first session.
  - Phase 2: orchestrator recycles (openCount → 2+, wrapper state
    reaches Broadcasting on the new session).
  - Phase 3: post-refresh frames round-trip on a FRESH listener
    subscription against the new session.
  - Wrapper invariant: outward state never surfaced
    Reconnecting / Failed during the recycle.

Both speaker interop tests pass against the real relay.

The "listener subscription survives publisher recycle" gap is a
real production concern for >9-min stage time but lives outside
this PR — it would need either listener-side announce-watching or
a coupled refresh-on-publisher-cycle hook in the listener wrapper.

https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
2026-04-28 13:02:29 +00:00
Claude 138ee12a6a fix(nests): align kind-4312 + kind-30312 wire format with nostrnests/EGG-07
After verifying the nostrnests reference (NestsUI-v2 @ main):
- ProfileCard.tsx writes kicks as ['action','kick'] tags with empty content
- useAdminCommands.ts reads action via tags.find(t => t==='action') and
  applies a 60-s relay since plus a processedRef Set to dedup re-deliveries
- p-tag role marker for moderators is 'admin', not 'moderator'

Amethyst was diverging on every one of those, which means our outbound
admin commands were invisible to nostrnests, theirs to us, and any
nostrnests admin (role='admin') failed our isModerator() / canSpeak()
gates entirely — kicks and force-mutes signed by them were silently
dropped.

Changes:

quartz/AdminCommandEvent.kt
  - Emit ['action', '<verb>'] tag with empty content
  - Reader prefers the tag, falls back to content for any in-flight
    Amethyst-built kick from before this commit
  - kick() and forceMute() share a common build() helper

quartz/ParticipantTag.kt
  - ROLE.MODERATOR.code = 'admin' (matches nostrnests + EGG-07)
  - Adds legacyCodes = ['moderator'] so older Amethyst-emitted
    kind-30312 events still parse as MODERATOR
  - effectiveRole() walks both code + legacyCodes

amethyst/AdminCommandsCollector
  - Filter carries since = now - 60 (EGG-07 #7)
  - Defensive per-event freshness re-check for cached events / clock skew
  - mutableSetOf<String>() processed-id dedup for the lifetime of the
    collector, mirroring useAdminCommands.ts's processedRef

EGG-07.md
  - Documents the Amethyst-only ['action','mute'] extension under a new
    'Implemented extensions' section. nostrnests doesn't emit or honour
    it today; cross-client force-mutes only work between Amethyst peers
  - 'warn' stays in the future-actions list — nostrnests has no plans
    for it either

Tests:
  - ParticipantTagTest: new asserts that 'admin' / 'Admin' / 'ADMIN'
    parse as ROLE.MODERATOR; pins the wire string to 'admin' and the
    legacy alias to 'moderator'
  - AdminCommandEventTest: kick/forceMute templates carry ['action', _]
    tag with empty content; legacy content-form still parses; tag wins
    over content when both are present
2026-04-28 12:59:48 +00:00
Crowdin Bot 64d56e2e2c New Crowdin translations by GitHub Action 2026-04-28 12:43:52 +00:00
Vitor Pamplona bc0179aac6 Merge pull request #2622 from vitorpamplona/claude/test-nests-amethyst-interop-WETiY
Add Nests audio-room interop test harness for Amethyst ↔ web
2026-04-28 08:42:15 -04:00
Claude d41a24f945 feat(nests): empty-stage hint, local hush, moderator/force-mute moderation
Three improvements stacked into one commit since they share files:

#3 Empty-stage hint: StageGrid no longer disappears when nobody is on
   stage. The "Stage" label stays and a quiet "Waiting for speakers…"
   line keeps the strip visible so the room doesn't look broken before
   the first speaker arrives.

#5 Local per-speaker hush: AudioPlayer gains a setVolume(Float)
   default-no-op method; AudioTrackPlayer composes mute and volume
   multiplicatively into AudioTrack.setVolume so a hushed stream stays
   silent regardless of mute state. NestViewModel exposes
   locallyHushed: ImmutableSet<String> in NestUiState plus
   setLocalHushed(pubkey, hushed) — applied at attach time so a
   re-subscribe of an already-hushed speaker stays silent. New "Hush
   this speaker" / "Restore this speaker" row in
   ParticipantHostActionsSheet, available to anyone (it affects only
   our own playback, nothing on the wire).

#4 Host moderation gaps: wires Promote-to-Moderator using the existing
   RoomParticipantActions.setRole(ROLE.MODERATOR) builder — UI gap
   only, no protocol change. Adds a new AdminCommandEvent.Action.MUTE
   variant + AdminCommandEvent.forceMute(room, target) builder; the
   sheet emits it on "Force-mute speaker" and the
   AdminCommandsCollector dispatches incoming MUTE actions to a new
   NestViewModel.onForceMuted() that routes through the existing
   setMicMuted(true) path. Honor-based, same trust model as KICK —
   relays don't enforce signer authority, the client checks the
   signer is host or moderator on the active kind-30312.
2026-04-28 12:41:23 +00:00
Claude bed1b328ce fix(nests): self-terminate the audio-level emitter when no one is talking
The previous emitter ran a permanent while(true){delay} loop on the
viewModelScope from connect-time onward. Cheap in production but
catastrophic for any test that calls runTest's advanceUntilIdle()
after vm.connect() — virtual time never reached idle, hanging
NestViewModelTest's existing connection-state cases for ~15 minutes
before the worker timeout killed them.

Now the emitter is started lazily by onAudioLevel() and exits the
loop the first tick after rawAudioLevels empties, so an idle room
schedules nothing. The next decoded frame restarts it. Idempotent,
so the launch path doesn't need to call it any more — removed the
boot from launchConnect().
2026-04-28 12:08:15 +00:00