Merge pull request #2621 from vitorpamplona/claude/review-nostr-nests-compliance-hKBnS

feat(nests): proactive JWT refresh + reconnect for speaker path
This commit is contained in:
Vitor Pamplona
2026-04-28 13:56:06 -04:00
committed by GitHub
15 changed files with 2543 additions and 149 deletions
@@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.AutoConnectAndTrackSpeakers
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.LeaveOnKick
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.LeaveOnRoomClosed
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.NestForegroundServiceLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.NestPresencePublisher
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.NestRoomEventCollectors
@@ -168,6 +169,11 @@ private fun NestActivityBody(
// Kick → leave the activity.
LeaveOnKick(viewModel, onLeave)
// Host ended the room (status: CLOSED) → leave the activity.
// Same teardown path as kick — VM.onCleared() releases the
// listener + speaker when the activity finishes.
LeaveOnRoomClosed(event, onLeave)
val ui by viewModel.uiState.collectAsState()
// System bridges: PIP overlay actions + foreground service.
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
import com.vitorpamplona.amethyst.service.nests.NestForegroundService
import com.vitorpamplona.nestsclient.NestsRoomConfig
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import kotlinx.coroutines.flow.SharedFlow
/**
@@ -86,6 +87,39 @@ internal fun LeaveOnKick(
LaunchedEffect(wasKicked) { if (wasKicked) onLeave() }
}
/**
* Bounce out of the room when the host flips the kind-30312
* `status` tag to CLOSED (or when [MeetingSpaceEvent.checkStatus]
* auto-closes a stale event past the 8 h cutoff). The relay
* subscription in [NestRoomFilterAssemblerSubscription] feeds
* LocalCache, `observeNoteEvent` upstream emits the new
* [MeetingSpaceEvent] reference, and this LaunchedEffect's key
* change triggers the leave.
*
* Without this, a host ending the room only takes effect for
* users who back out manually — every other listener / speaker
* stays connected to the relay until either the next JWT
* expiry or `onCleared()` fires when they finally close the
* activity. Users hear silence (no one's publishing) but the
* UI shows them as still "in" the room.
*
* Triggers only when the live event's status is CLOSED; an
* initial event in any other state (PLANNED, OPEN, PRIVATE)
* is a no-op so the room can finish its connect flow first.
*/
@Composable
internal fun LeaveOnRoomClosed(
event: MeetingSpaceEvent,
onLeave: () -> Unit,
) {
// `event.isLive()` returns false on CLOSED. Keying on that
// boolean (rather than the event reference) means the effect
// fires once per live→closed transition, regardless of how
// many other tag changes the host pushes alongside.
val isLive = event.isLive()
LaunchedEffect(isLive) { if (!isLive) onLeave() }
}
/**
* Bridge between the in-Compose VM state and the Activity-level
* Picture-in-Picture controller: pushes mute / connected state UP