feat(audio-rooms): expose presences StateFlow on AudioRoomViewModel
Adds the listener-side fan-in API the participant grid + listener counter consume: AudioRoomViewModel.presences: StateFlow<Map<String, RoomPresence>> AudioRoomViewModel.onPresenceEvent(MeetingRoomPresenceEvent) AudioRoomViewModel.evictStalePresences(olderThanSec: Long) The platform layer (amethyst, next commit) observes LocalCache for `kinds=[10312], #a=[roomATag]` and pipes events through onPresenceEvent; that keeps the data source platform-specific and lets commons stay free of Android-only LocalCache references. evictStalePresences runs on a periodic tick driven by the platform layer. Bug-fix while wiring this: the initial RoomPresence had a custom pubkey-only equals() (intended to make Set<RoomPresence> dedup straightforwardly). That broke Map<String, RoomPresence>.equals on a heartbeat-only update — old.equals(new) returned true even when handRaised / publishing flipped, so StateFlow suppressed the emission and the UI never saw the change. Reverted to the data-class all-fields equals; the Map is keyed by the pubkey String anyway, so the custom equals never bought us anything. Test `roomPresenceEqualityIsAllFields` pins the contract so it can't drift back. Tests: * onPresenceEventPopulatesPresencesMapAndDedupesByPubkey * evictStalePresencesDropsOldPeers
This commit is contained in:
+63
@@ -151,6 +151,69 @@ class AudioRoomViewModelTest {
|
||||
assertTrue(vm.uiState.value.onStageNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onPresenceEventPopulatesPresencesMapAndDedupesByPubkey() =
|
||||
runTest {
|
||||
val vm = newViewModel { FakeNestsListener() }
|
||||
|
||||
val alice = "a".repeat(64)
|
||||
val bob = "b".repeat(64)
|
||||
|
||||
fun ev(
|
||||
pk: String,
|
||||
createdAt: Long,
|
||||
handRaised: Boolean,
|
||||
) = com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent(
|
||||
id = "0".repeat(64),
|
||||
pubKey = pk,
|
||||
createdAt = createdAt,
|
||||
tags = arrayOf(arrayOf("a", "30312:host:room"), arrayOf("hand", if (handRaised) "1" else "0")),
|
||||
content = "",
|
||||
sig = "0".repeat(128),
|
||||
)
|
||||
|
||||
vm.onPresenceEvent(ev(alice, 100L, handRaised = false))
|
||||
vm.onPresenceEvent(ev(bob, 100L, handRaised = false))
|
||||
vm.onPresenceEvent(ev(alice, 200L, handRaised = true))
|
||||
|
||||
val snap = vm.presences.value
|
||||
assertEquals(setOf(alice, bob), snap.keys)
|
||||
assertTrue(snap[alice]!!.handRaised)
|
||||
assertEquals(200L, snap[alice]!!.updatedAtSec)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun evictStalePresencesDropsOldPeers() =
|
||||
runTest {
|
||||
val vm = newViewModel { FakeNestsListener() }
|
||||
val alice = "a".repeat(64)
|
||||
val bob = "b".repeat(64)
|
||||
|
||||
vm.onPresenceEvent(
|
||||
com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent(
|
||||
id = "0".repeat(64),
|
||||
pubKey = alice,
|
||||
createdAt = 100L,
|
||||
tags = arrayOf(arrayOf("a", "30312:host:room")),
|
||||
content = "",
|
||||
sig = "0".repeat(128),
|
||||
),
|
||||
)
|
||||
vm.onPresenceEvent(
|
||||
com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent(
|
||||
id = "0".repeat(64),
|
||||
pubKey = bob,
|
||||
createdAt = 500L,
|
||||
tags = arrayOf(arrayOf("a", "30312:host:room")),
|
||||
content = "",
|
||||
sig = "0".repeat(128),
|
||||
),
|
||||
)
|
||||
|
||||
vm.evictStalePresences(olderThanSec = 400L)
|
||||
assertEquals(setOf(bob), vm.presences.value.keys)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publishingNowDerivesFromBroadcastStateAndMute() {
|
||||
// Idle / connecting / failed: never publishing.
|
||||
|
||||
+7
-5
@@ -116,12 +116,14 @@ class RoomPresenceStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roomPresenceEqualityIsPubkeyOnly() {
|
||||
// Two snapshots of the same peer at different times are
|
||||
// "equal" so a Set<RoomPresence> deduplicates correctly.
|
||||
fun roomPresenceEqualityIsAllFields() {
|
||||
// Two snapshots of the same peer at different timestamps are
|
||||
// NOT equal — Map<String, RoomPresence>.equals(Map) reaches
|
||||
// value.equals(value); a pubkey-only equals would suppress
|
||||
// StateFlow emissions on heartbeat-only updates because
|
||||
// old.equals(new) would return true.
|
||||
val a1 = RoomPresence.from(event(alice, 100L, handRaised = false))
|
||||
val a2 = RoomPresence.from(event(alice, 200L, handRaised = true))
|
||||
assertEquals(a1, a2)
|
||||
assertEquals(a1.hashCode(), a2.hashCode())
|
||||
assertEquals(false, a1 == a2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user