fix(nests): only gate room re-entry through lobby when card reads closed

NestFeedCard previously routed every tap on a joinable MeetingSpaceEvent
through NestLobbyScreen. The lobby exists to keep an accidental tap on
a stale room from booting the audio pipeline (and the host-side
kind-30312 republish path) — once the badge says the room is live with
fresh presence, that gating is just extra friction. Mirror the badge's
liveness heuristic in the click handler and launch NestActivity
directly for cards rendering as LIVE / PLANNED / PRIVATE; only EndedFlag
cards (status = ENDED, status = LIVE with stale presence, or unknown
status) keep flowing through the read-only lobby.
This commit is contained in:
Claude
2026-04-30 22:25:39 +00:00
parent 3dceb194dc
commit 897e036b3a
@@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -77,6 +78,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53L
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestBridge
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@@ -134,11 +137,15 @@ fun NestsFeedLoaded(
} }
/** /**
* Audio-rooms list card. Mirrors [ObserveAndRenderSpace] visually but * Audio-rooms list card. Mirrors [ObserveAndRenderSpace] visually and
* routes a tap into the in-app [NestLobbyScreen] when the underlying * gates a tap on the underlying [MeetingSpaceEvent] by the same liveness
* event is a joinable [MeetingSpaceEvent]. The lobby is read-only — * heuristic the badge uses: cards rendering as [EndedFlag] (status =
* launching the audio activity (and any host-side kind-30312 refresh) * ENDED, status = LIVE with stale presence, or unknown status) route
* still requires an explicit tap on the lobby's Open Room button. * through the read-only [NestLobbyScreen] so re-entry can't accidentally
* boot the audio pipeline or trigger a host-side kind-30312 refresh on
* a dead room. Cards that the UI still shows as live, scheduled, or
* private launch [NestActivity] directly — the lobby's purpose is only
* to gate stale rooms.
*/ */
@Composable @Composable
private fun NestFeedCard( private fun NestFeedCard(
@@ -148,15 +155,44 @@ private fun NestFeedCard(
nav: INav, nav: INav,
) { ) {
val meetingEvent = baseNote.event as? MeetingSpaceEvent ?: return val meetingEvent = baseNote.event as? MeetingSpaceEvent ?: return
val context = LocalContext.current
val status = meetingEvent.checkStatus(meetingEvent.status())
val isUiClosed =
when (status) {
StatusTag.STATUS.LIVE -> {
val latestPresence by observeRoomLatestPresence(meetingEvent.address(), accountViewModel)
val presence = latestPresence
presence != null && presence <= TimeUtils.now() - PRESENCE_FRESHNESS_WINDOW_SECONDS
}
StatusTag.STATUS.PLANNED,
StatusTag.STATUS.PRIVATE,
-> {
false
}
else -> {
true
}
}
val onClick = val onClick =
remember(meetingEvent) { remember(meetingEvent, isUiClosed) {
{ {
val service = meetingEvent.service() val service = meetingEvent.service()
val endpoint = meetingEvent.endpoint() val endpoint = meetingEvent.endpoint()
val dTag = meetingEvent.address().dTag val dTag = meetingEvent.address().dTag
if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) { if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) {
nav.nav(Route.NestLobby(meetingEvent.address().toValue())) if (isUiClosed) {
nav.nav(Route.NestLobby(meetingEvent.address().toValue()))
} else {
NestBridge.set(accountViewModel)
NestActivity.launch(
context = context,
addressValue = meetingEvent.address().toValue(),
)
}
} else { } else {
nav.nav { routeFor(baseNote, accountViewModel.account) } nav.nav { routeFor(baseNote, accountViewModel.account) }
} }