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
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
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
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
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.
#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.
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
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.
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().
Adds an end-to-end audio-amplitude pipeline so on-stage speakers'
green ring throbs in time with their voice (closer to Spaces /
Clubhouse than the previous binary "is in speakingNow" indicator).
NestPlayer.play() now takes an onLevel callback and computes the
normalized peak of each decoded 16-bit PCM frame. NestViewModel
exposes audioLevels: StateFlow<Map<String, Float>>; raw 50 Hz
updates from the decode loop are coalesced into a 10 Hz publish
tick (LEVEL_TICK_MS) so the StateFlow doesn't spam recompositions
across a busy stage. The map is cleared on speaker close, on the
speaking-timeout sweep, and on teardown.
In MemberCell the speaking ring's width animates between
AVATAR_RING_WIDTH (3 dp) and MAX_RING_WIDTH (7 dp) via
animateDpAsState, smoothing the tick into a continuous halo.
Muted-publisher and idle states are unchanged.
Adds NestPlayerTest coverage for the new callback (level math +
empty-PCM short-circuit).
- Cap mic, hand-raise, and role badges at 28.dp so they no longer
dominate the new 100.dp avatars.
- Surface HOST and MODERATOR with a small badge anchored top-left
(purple medal / sky shield), mirroring nostrnests at-a-glance.
- TalkBack: MicStateBadge announces "Speaking now" / "Microphone
muted" / "Microphone open" instead of going silent.
- StageGrid floats currently-speaking members to the top so the
listener doesn't scroll to find who they're hearing; AudienceGrid
floats raised hands so moderators can approve the queue without
hunting. Both use stable sorts and Modifier.animateItem() for a
smooth shuffle.
Adds cli/tests/nests/nests-interop.sh — a 47-test operator-driven
interop script that walks a tester through every audio-room edge case
between Amethyst Android and the nostrnests.com NestsUI-v2 web client.
Coverage: bidirectional host/listener flows, audio round-trip on
moq-lite Lite-03, hand-raise + role promotion/demotion, mute, kind:7
reactions (including NIP-30 custom emoji + 30 s overlay clear), kind:1311
in-room chat (text + emoji + link + image upload + history backfill),
kind:4312 kick, room edit + close, scheduled rooms (status=planned),
multi-speaker, background audio + PIP, network drop reconnect, 10-min
JWT refresh, custom moq servers (kind:10112), naddr deep links, and
edge cases (long titles, empty rooms, leave-stage, dedupe, race).
The script follows the marmot/marmot-interop.sh pattern: shared
logging + result helpers, color-coded prompts (yellow=Amethyst,
magenta=web, cyan=optional 3rd identity), p/f/s confirms recorded to a
TSV, and a final summary table. Skip / only / keep-state flags are
supported for iteration.
Pure manual harness — amy does not yet ship `amy nests <verb>` so
nothing is automatable from the CLI side.
Drops STAGE_CELL_MIN and AUDIENCE_CELL_MIN from 112.dp to 96.dp, which
lets a Pixel-class 411.dp phone fit 4 cells per row instead of 3 (and
unlocks 5/6 columns earlier on tablets). The 100.dp avatars overflow
~2.dp into the 6.dp horizontal arrangement on each side, so neighbors
still show a ~2.dp visible gap. Names ellipsize to the cell width as
before.
100dp avatars across stage and audience tabs, names centered under each
cell, and the avatar border now distinguishes actively speaking (green)
from publishing-but-muted (red) at a glance — the existing mic-pill
keeps its semantics underneath.
UsernameDisplay gains an optional textAlign so the grid can opt in to
centered labels without disturbing other call sites.
The activity class lives in `…nests.room.activity.NestActivity` but the
manifest entry was missing the `.activity` segment, so tapping a Nests
feed card crashed with ActivityNotFoundException.
AppBottomBar hides itself on canPop entries by emitting nothing into the
bar slot. DisappearingScaffold previously read the slot's measured
height as 0 and skipped applying navigationBarsPadding (its
`bottomBar == null` branch only fires when the lambda itself is null),
which let the FAB and feed content slide under the system navigation
bar. Fall back to the system-nav-bar inset when the slot measures zero
so the FAB lines up with the bar-visible position and content stays
clear of the gesture/3-button nav.
https://claude.ai/code/session_01QSk7CnjNtbcgS3XBD5WEZy
Drop the dedicated NestThumbStatusFilterAssembler and have the
thumb's liveness gate piggyback on NestRoomFilterAssembler, which
already subscribes to kinds=[1311, 10312, 7] + admin (4312) per
room a-tag. observeRoomLatestPresence is now a private composable
in NestsFeedLoaded.kt that opens NestRoomFilterAssemblerSubscription
and reads the latest cached MeetingRoomPresenceEvent createdAt off
the room's LiveActivitiesChannel.
Trade-off: the thumb pays for the room's full chat + reaction
history, not just presence. Bounded by LazyColumn — only currently-
composed thumbs hold subscriptions — and the cache it warms is
exactly what NestActivity needs when the user opens the room. If
wire load becomes a concern, swap to a limit:1 lite mode on the
existing assembler.
Net: -159 / +51 lines, one less filter assembler in the coordinator.
Extends the canPop FAB-position fix to every screen that hosts a
floating action button: Pictures, Videos, Articles, Longs, Shorts,
Polls, Products, Communities, Nests, Badges, Discover, WebBookmarks,
Messages, Home, GeoHash, Hashtag, Relay, Community, OldBookmarks,
EmojiPack, ChessLobby, MarmotGroups, the bookmark management screens
and the migration FAB. Adds a small FabBottomBarPadded helper so call
sites that use named FAB composables stay tidy.
https://claude.ai/code/session_01QSk7CnjNtbcgS3XBD5WEZy
The kind-30312 status tag goes stale silently when a host crashes
without publishing a CLOSED replacement, so checkStatus()'s 8-hour
fallback was the only liveness signal — abandoned rooms still
showed LiveFlag for up to 8h after the audio stopped.
Adds a per-thumb relay probe: kinds=[10312], #a=[roomATag], limit=1.
The relay returns the single most-recent presence event among all
speakers in the room, then keeps streaming so fresh heartbeats land
in real time. Speakers re-emit kind-10312 every ~60 s while
publishing, so a presence newer than 180 s means at least one
speaker is still in the room; older flips the badge to EndedFlag
even though the 30312 event still says OPEN.
UX: optimistic on first paint (LiveFlag while we have no presence
data), self-corrects within one round-trip once the relay replies.
Files:
- NestThumbStatusFilterAssembler.kt: per-room limit:1 REQ +
observeNestRoomLatestPresence composable that scans the
LiveActivitiesChannel's notes for max(createdAt) of cached
presence events.
- RelaySubscriptionsCoordinator.kt: register nestThumbStatus.
- NestsFeedLoaded.kt: thread card.id into a private
RenderLiveOrEndedFromPresence helper that gates the OPEN badge
on freshness.
AppBottomBar returns nothing on canPop entries (drawer pushes, in-app
navigations), so the floating action button drops by the bar's height
when navigating into a screen and rises again on a tab root. Reserve the
50dp gap at the FAB site via a fabBottomBarPadding(nav) modifier so the
button stays at a consistent vertical position regardless of the bar's
visibility on EmojiPacks, BookmarkGroups, and InterestSets.
https://claude.ai/code/session_01QSk7CnjNtbcgS3XBD5WEZy
NestsFeedLoaded.RenderLiveSpacesThumb gates LiveFlag/EndedFlag on
the kind-30312 status tag verbatim, so a host that crashed without
publishing a CLOSED replacement leaves the room flagged "live"
indefinitely. checkStatus() applies the same 8-hour age-out the
event's own helper uses, so abandoned OPEN rooms surface as
EndedFlag instead.
Also drop a stray println debug ("AABBCC …") in the same map block.
Mirrors the Profile Edit banner upload: a SelectSingleFromGallery
leadingIcon on the Cover Image field picks media, strips metadata,
compresses, and uploads to the user's configured Blossom (or NIP-96)
server. The returned URL fills the field so the existing publish path
tags it on the kind-30312 MeetingSpaceEvent.
The previous refactor renamed EditNestViewModelTest and
RoomParticipantActionsTest into edit/ and participants/ subdirs but
the package declarations stayed at the old flat path, so the tests
no longer compiled against the relocated production code.
The nests/room/ package had grown to 23+ files in one flat directory,
making it hard to find related composables and lifecycle helpers. Each
file is now grouped by responsibility:
activity/ Android Activity entry + AccountViewModel bridge
lobby/ Pre-join lobby card (NestJoinCard, JoinNestButton)
lifecycle/ Side-effect composables wiring VM <-> system / cache
screen/ Top-level layouts (FullScreen, PipScreen, ActionBar)
stage/ Speaker / audience grids and overlays
participants/ Per-participant host actions + role mutations
reactions/ Reaction picker
chat/ Chat panel and composer (also absorbs the old send/)
edit/ Edit-room sheet + ViewModel
theme/ Themed scope + URL font loader
Also drops the unused StagePeopleRow composable from NestCommon and
inlines its remaining one-liner connectingLabel helper into
NestActionBar (the only caller), removing NestCommon entirely.
External imports in 7 sibling files were updated to point at the new
subpackage paths.
Resolves NestFullScreen.kt by keeping the redesigned layout (header
strip, stage grid, tabs, sticky NestActionBar) while dropping the
NestThemedScope wrapper that main removed in ff00dcf3 ("screens get
too dark"). The RoomTheme import goes with it; the stale doc-comment
reference to NestThemedScope is updated accordingly.