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:
+18
-18
@@ -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.
|
||||
*/
|
||||
+12
-12
@@ -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++
|
||||
Reference in New Issue
Block a user