refactor: rename Audio Room → Nest project-wide

Aligns class names, package paths, string resource keys, UI text and
intent actions with the Nests branding used by the EGG specs in
nestsClient/specs/. Mechanical rename — no behavior change.

- Folders: audiorooms/ → nests/ (5 paths across amethyst, quartz)
- 30+ class renames (AudioRoom* → Nest*, AudioRooms* → Nests*)
- String resource keys audio_room_* → nest_*
- UI strings "Audio Room"/"Audio Rooms" → "Nest"/"Nests" (incl. all locales)
- Intent extras AUDIO_ROOM_* → NEST_*
- Compose route Route.AudioRooms → Route.Nests

Spec-aligned identifiers (MeetingSpaceEvent, meetingSpaces/, the nip53*
packages) are intentionally untouched — those are NIP-53 protocol
names, not "audio room" branding.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
This commit is contained in:
Claude
2026-04-27 14:36:50 +00:00
parent 208c90b246
commit dc3ac31ae4
74 changed files with 565 additions and 565 deletions
@@ -33,7 +33,7 @@ import com.vitorpamplona.nestsclient.NestsSpeaker
import com.vitorpamplona.nestsclient.NestsSpeakerState
import com.vitorpamplona.nestsclient.audio.AudioCapture
import com.vitorpamplona.nestsclient.audio.AudioPlayer
import com.vitorpamplona.nestsclient.audio.AudioRoomPlayer
import com.vitorpamplona.nestsclient.audio.NestPlayer
import com.vitorpamplona.nestsclient.audio.OpusDecoder
import com.vitorpamplona.nestsclient.audio.OpusEncoder
import com.vitorpamplona.nestsclient.connectNestsSpeaker
@@ -60,7 +60,7 @@ import kotlin.coroutines.cancellation.CancellationException
* Per-screen state holder for a NIP-53 audio-room.
*
* Owns one [NestsListener] for the lifetime of the screen and one
* [AudioRoomPlayer] per active speaker subscription. The screen tells the
* [NestPlayer] per active speaker subscription. The screen tells the
* VM which speakers it cares about via [updateSpeakers]; subscribe / play
* happen automatically once the listener is [NestsListenerState.Connected].
*
@@ -90,7 +90,7 @@ import kotlin.coroutines.cancellation.CancellationException
* via `viewModelScope.launch(Dispatchers.Main.immediate) { ... }` first.
*/
@Stable
class AudioRoomViewModel(
class NestViewModel(
private val httpClient: NestsClient,
private val transport: WebTransportFactory,
private val decoderFactory: () -> OpusDecoder,
@@ -116,8 +116,8 @@ class AudioRoomViewModel(
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
private val cleanupScope: CoroutineScope = GlobalScope,
) : ViewModel() {
private val _uiState = MutableStateFlow(AudioRoomUiState())
val uiState: StateFlow<AudioRoomUiState> = _uiState.asStateFlow()
private val _uiState = MutableStateFlow(NestUiState())
val uiState: StateFlow<NestUiState> = _uiState.asStateFlow()
/**
* Listener-side aggregation of every peer's most recent kind-10312
@@ -292,7 +292,7 @@ class AudioRoomViewModel(
/**
* Toggle whether we hold a speaker slot. Tier-2's "leave the stage"
* tap flips this to `false`; the next kind-10312 heartbeat picks up
* the change via [AudioRoomUiState.onStageNow]. Does NOT stop a
* the change via [NestUiState.onStageNow]. Does NOT stop a
* running broadcast UI / caller is expected to call
* [BroadcastHandle.close] separately when leaving the stage.
*/
@@ -674,7 +674,7 @@ class AudioRoomViewModel(
/**
* Stop the per-speaker player + fire-and-forget UNSUBSCRIBE on the VM
* scope. Both `AudioRoomPlayer.stop()` and `SubscribeHandle.unsubscribe()`
* scope. Both `NestPlayer.stop()` and `SubscribeHandle.unsubscribe()`
* are suspend, so we route them through one coroutine instead of two.
*/
private fun closeSubscription(slot: ActiveSubscription) {
@@ -766,7 +766,7 @@ class AudioRoomViewModel(
}
try {
val isMuted = _uiState.value.isMuted
val roomPlayer = AudioRoomPlayer(decoder, player, viewModelScope)
val roomPlayer = NestPlayer(decoder, player, viewModelScope)
// Apply current mute state before play() opens the device so the
// first frame respects it.
player.setMutedSafe(isMuted)
@@ -834,7 +834,7 @@ class AudioRoomViewModel(
// listener.close() below tears down the MoQ session and drops
// every active subscription, so we don't need to call
// unsubscribe() per-handle here — but we DO need to await each
// AudioRoomPlayer.stop() so the native MediaCodec / AudioTrack
// NestPlayer.stop() so the native MediaCodec / AudioTrack
// is released after its decode loop has unwound.
val scope = if (finalCleanup) cleanupScope else viewModelScope
val players = activeSubscriptions.values.map { it.detach().first }
@@ -912,7 +912,7 @@ class AudioRoomViewModel(
val pubkey: String,
) {
private var handle: SubscribeHandle? = null
private var roomPlayer: AudioRoomPlayer? = null
private var roomPlayer: NestPlayer? = null
var player: AudioPlayer? = null
private set
var isPlaying: Boolean = false
@@ -920,7 +920,7 @@ class AudioRoomViewModel(
fun attach(
handle: SubscribeHandle,
roomPlayer: AudioRoomPlayer,
roomPlayer: NestPlayer,
player: AudioPlayer,
) {
this.handle = handle
@@ -931,12 +931,12 @@ class AudioRoomViewModel(
/**
* Hand the player + handle back to the caller's coroutine scope
* `AudioRoomPlayer.stop()` and `SubscribeHandle.unsubscribe()` are
* `NestPlayer.stop()` and `SubscribeHandle.unsubscribe()` are
* both suspend, and the caller has the right scope to await them
* (so native MediaCodec/AudioTrack release runs after the decode
* loop has unwound, per audit MoQ #11/#12).
*/
fun detach(): Pair<AudioRoomPlayer?, SubscribeHandle?> {
fun detach(): Pair<NestPlayer?, SubscribeHandle?> {
isPlaying = false
val p = roomPlayer
val h = handle
@@ -951,7 +951,7 @@ class AudioRoomViewModel(
}
}
// Platform-specific Factory lives in `amethyst/.../audiorooms/room/`,
// Platform-specific Factory lives in `amethyst/.../nests/room/`,
// not commonMain — the lifecycle KMP `ViewModelProvider.Factory`
// signature has been migrating between releases; a thin Android-side
// factory keeps that churn out of shared code.
@@ -1006,7 +1006,7 @@ private fun AudioPlayer.setMutedSafe(muted: Boolean) {
}
@Immutable
data class AudioRoomUiState(
data class NestUiState(
val connection: ConnectionUiState = ConnectionUiState.Idle,
val isMuted: Boolean = false,
/** Pubkeys we have an open MoQ subscription for. */
@@ -1025,11 +1025,11 @@ data class AudioRoomUiState(
* speaker subsequently goes quiet. Cleared on speaker removal.
*/
val connectingSpeakers: ImmutableSet<String> = persistentSetOf(),
/** Speaker / publisher state — only relevant when [AudioRoomViewModel.canBroadcast]. */
/** Speaker / publisher state — only relevant when [NestViewModel.canBroadcast]. */
val broadcast: BroadcastUiState = BroadcastUiState.Idle,
/**
* `true` when we hold a speaker slot vs being pure audience. Defaults
* to `true` for users with [AudioRoomViewModel.canBroadcast]; the
* to `true` for users with [NestViewModel.canBroadcast]; the
* "leave the stage" tap (Tier 2) flips it to `false`. Drives the
* `["onstage", "0|1"]` tag on emitted kind-10312 presence events.
*/
@@ -1076,7 +1076,7 @@ const val SPEAKING_TIMEOUT_MS: Long = 250L
/**
* How long a kind-7 reaction stays in
* [AudioRoomViewModel.recentReactions] before the eviction sweep
* [NestViewModel.recentReactions] before the eviction sweep
* drops it. Matches the duration of the floating-up animation in the
* SpeakerReactionOverlay.
*/
@@ -54,7 +54,7 @@ import kotlin.test.assertIs
import kotlin.test.assertTrue
/**
* Drives [AudioRoomViewModel] with a fake [NestsListenerConnector] and
* Drives [NestViewModel] with a fake [NestsListenerConnector] and
* verifies its state-flow transitions:
* - connect() shows Connecting before the underlying connector returns
* - listener emits Connected uiState collapses to Connected
@@ -62,13 +62,13 @@ import kotlin.test.assertTrue
* - setMuted updates uiState and propagates to active subscriptions
* - disconnect() returns to Idle and tears down the listener
*
* Subscribe-side (subscribeSpeaker AudioRoomPlayer.play) is exercised via
* Subscribe-side (subscribeSpeaker NestPlayer.play) is exercised via
* the existing nestsClient tests; the cross-module audio glue would need
* an internal SubscribeHandle constructor that's intentionally not visible
* here.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class AudioRoomViewModelTest {
class NestViewModelTest {
@BeforeTest
fun setupMainDispatcher() {
Dispatchers.setMain(UnconfinedTestDispatcher())
@@ -143,7 +143,7 @@ class AudioRoomViewModelTest {
// Defaults to true so a freshly-joined speaker advertises
// onstage=1 on the first heartbeat (the heartbeat in
// AudioRoomActivityContent reads this as the tag value).
// NestActivityContent reads this as the tag value).
assertTrue(vm.uiState.value.onStageNow)
vm.setOnStage(false)
assertFalse(vm.uiState.value.onStageNow)
@@ -332,20 +332,20 @@ class AudioRoomViewModelTest {
@Test
fun publishingNowDerivesFromBroadcastStateAndMute() {
// Idle / connecting / failed: never publishing.
assertFalse(AudioRoomUiState().publishingNow)
assertFalse(AudioRoomUiState(broadcast = BroadcastUiState.Connecting).publishingNow)
assertFalse(AudioRoomUiState(broadcast = BroadcastUiState.Failed("boom")).publishingNow)
assertFalse(NestUiState().publishingNow)
assertFalse(NestUiState(broadcast = BroadcastUiState.Connecting).publishingNow)
assertFalse(NestUiState(broadcast = BroadcastUiState.Failed("boom")).publishingNow)
// Broadcasting unmuted: publishing.
assertTrue(
AudioRoomUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = false)).publishingNow,
NestUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = false)).publishingNow,
)
// Broadcasting muted: NOT publishing — matches the wire-tag
// semantics ("publishing=1" means actually pushing packets,
// not "hold a slot but silenced").
assertFalse(
AudioRoomUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = true)).publishingNow,
NestUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = true)).publishingNow,
)
}
@@ -425,8 +425,8 @@ class AudioRoomViewModelTest {
)
}
private fun TestScope.newViewModel(connect: suspend (CoroutineScope) -> NestsListener): AudioRoomViewModel =
AudioRoomViewModel(
private fun TestScope.newViewModel(connect: suspend (CoroutineScope) -> NestsListener): NestViewModel =
NestViewModel(
httpClient = NoopNestsClient,
transport = NoopWebTransportFactory,
decoderFactory = { NoopOpusDecoder },
@@ -452,7 +452,7 @@ class AudioRoomViewModelTest {
mutable.value = s
}
override suspend fun subscribeSpeaker(speakerPubkeyHex: String): SubscribeHandle = error("subscribeSpeaker not exercised in these tests — see AudioRoomPlayerTest in :nestsClient")
override suspend fun subscribeSpeaker(speakerPubkeyHex: String): SubscribeHandle = error("subscribeSpeaker not exercised in these tests — see NestPlayerTest in :nestsClient")
override suspend fun close() {
closeCallCount++