From 897e036b3a72a8161dbb172be3ca3b926ca19b98 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 22:25:39 +0000 Subject: [PATCH] fix(nests): only gate room re-entry through lobby when card reads closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../screen/loggedIn/nests/NestsFeedLoaded.kt | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 5107568f8..5b98e1ca3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp 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.discover.nip53LiveActivities.LoadParticipants 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.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer @@ -134,11 +137,15 @@ fun NestsFeedLoaded( } /** - * Audio-rooms list card. Mirrors [ObserveAndRenderSpace] visually but - * routes a tap into the in-app [NestLobbyScreen] when the underlying - * event is a joinable [MeetingSpaceEvent]. The lobby is read-only — - * launching the audio activity (and any host-side kind-30312 refresh) - * still requires an explicit tap on the lobby's Open Room button. + * Audio-rooms list card. Mirrors [ObserveAndRenderSpace] visually and + * gates a tap on the underlying [MeetingSpaceEvent] by the same liveness + * heuristic the badge uses: cards rendering as [EndedFlag] (status = + * ENDED, status = LIVE with stale presence, or unknown status) route + * 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 private fun NestFeedCard( @@ -148,15 +155,44 @@ private fun NestFeedCard( nav: INav, ) { 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 = - remember(meetingEvent) { + remember(meetingEvent, isUiClosed) { { val service = meetingEvent.service() val endpoint = meetingEvent.endpoint() val dTag = meetingEvent.address().dTag 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 { nav.nav { routeFor(baseNote, accountViewModel.account) } }