46f693800d
Listener side now subscribes to every host AND speaker on the stage (was just hosts) and exposes a `speakingNow: ImmutableSet<String>` derived from MoQ object arrival. Each on-stage avatar gets a primary-color ring while its track is delivering audio (debounced 250 ms — ~12 Opus frames — so packet jitter doesn't make the ring flicker). ViewModel: - `AudioRoomViewModel.uiState` gains `speakingNow`. - New `onSpeakerActivity(pubkey)` is invoked once per received MoQ object via a `Flow.onEach` tap on `SubscribeHandle.objects` (before the AudioRoomPlayer consumes it). Each invocation (re)arms a per-speaker expiry coroutine that clears the entry after `SPEAKING_TIMEOUT_MS`. - speakingNow is cleared whenever the underlying subscription closes (speaker removed from room) or the listener tears down (disconnect / onCleared). Screen: - AudioRoomViewModel is now hoisted up to AudioRoomStageContent so the speaker rows can read speakingNow alongside the connection UI. - StagePeopleRow wraps each ClickableUserPicture in a 2 dp circular border when the participant is in `speakingNow`. - updateSpeakers now receives `(hosts + speakers).pubKeys` so dynamic speaker promotion via NIP-53 event updates flows through to the reconcile loop already in M1. Test: - Adds `speakingNowClearsOnTeardown` to AudioRoomViewModelTest. The per-frame activity path is exercised end-to-end by the existing AudioRoomPlayerTest in :nestsClient (which uses the in-memory pipe). Verified: `./gradlew spotlessApply :commons:jvmTest :nestsClient:jvmTest :amethyst:compilePlayDebugKotlin` green.