Stop nearby notes while app is backgrounded

This commit is contained in:
callebtc 2026-07-27 01:51:44 +02:00
parent 6fad768a3d
commit 416dc989e8
3 changed files with 65 additions and 5 deletions

View File

@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.asStateFlow
*
* Merely rendering the mesh timeline must not open a building-precision Nostr
* subscription. A subscription is eligible only after an explicit reveal and
* while at least one nearby-notes surface is active.
* while the app is foregrounded and at least one nearby-notes surface is active.
*/
@MainThread
class NearbyNotesController internal constructor(
@ -25,6 +25,7 @@ class NearbyNotesController internal constructor(
private var activeHolders = 0
private var locationEnabled = false
private var locationAuthorized = false
private var appForeground = false
private var buildingGeohash: String? = null
private var subscribedGeohash: String? = null
@ -50,6 +51,12 @@ class NearbyNotesController internal constructor(
reconcileSubscription()
}
/** Closes the live subscription whenever the process leaves the foreground. */
fun updateAppForeground(isForeground: Boolean) {
appForeground = isForeground
reconcileSubscription()
}
/**
* Updates the privacy-sensitive inputs independently of view activation.
* Permission revocation, location disable, or loss of the building cell
@ -78,6 +85,7 @@ class NearbyNotesController internal constructor(
private fun reconcileSubscription() {
val target = buildingGeohash.takeIf {
activeHolders > 0 &&
appForeground &&
_revealed.value &&
locationEnabled &&
locationAuthorized

View File

@ -29,6 +29,10 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.zIndex
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.R
import com.bitchat.android.geohash.ChannelID
@ -117,6 +121,30 @@ fun ChatScreen(viewModel: ChatViewModel) {
selectedPrivatePeer == null &&
privateChatSheetPeer == null
val processLifecycleOwner = remember { ProcessLifecycleOwner.get() }
DisposableEffect(processLifecycleOwner, nearbyNotesController) {
val lifecycle = processLifecycleOwner.lifecycle
val observer = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
nearbyNotesController.updateAppForeground(true)
}
override fun onStop(owner: LifecycleOwner) {
nearbyNotesController.updateAppForeground(false)
}
}
lifecycle.addObserver(observer)
nearbyNotesController.updateAppForeground(
lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED),
)
onDispose {
lifecycle.removeObserver(observer)
nearbyNotesController.updateAppForeground(false)
}
}
DisposableEffect(
isMeshTimeline,
locationEnabled,

View File

@ -16,9 +16,13 @@ class NearbyNotesControllerTest {
unsubscribe = { unsubscribeCount += 1 },
)
private fun foregroundController() = controller().also {
it.updateAppForeground(true)
}
@Test
fun `active mesh timeline does not subscribe before explicit reveal`() {
val controller = controller()
val controller = foregroundController()
controller.updateAvailability(
locationEnabled = true,
@ -38,7 +42,7 @@ class NearbyNotesControllerTest {
@Test
fun `reveal remains dormant until a nearby notes surface is active`() {
val controller = controller()
val controller = foregroundController()
controller.updateAvailability(true, true, "u4pruydq")
controller.reveal()
@ -52,7 +56,7 @@ class NearbyNotesControllerTest {
@Test
fun `last deactivate unsubscribes exactly once`() {
val controller = controller()
val controller = foregroundController()
controller.updateAvailability(true, true, "u4pruydq")
controller.reveal()
controller.activate()
@ -67,9 +71,28 @@ class NearbyNotesControllerTest {
assertEquals(1, unsubscribeCount)
}
@Test
fun `backgrounding closes the subscription and foregrounding restores it`() {
val controller = foregroundController()
controller.updateAvailability(true, true, "u4pruydq")
controller.activate()
controller.reveal()
controller.updateAppForeground(false)
assertEquals(1, unsubscribeCount)
assertTrue(controller.revealed.value)
controller.updateAppForeground(false)
assertEquals(1, unsubscribeCount)
controller.updateAppForeground(true)
assertEquals(listOf("u4pruydq", "u4pruydq"), subscriptions)
}
@Test
fun `disable and permission revocation close the live subscription`() {
val controller = controller()
val controller = foregroundController()
controller.updateAvailability(true, true, "u4pruydq")
controller.activate()
controller.reveal()
@ -91,6 +114,7 @@ class NearbyNotesControllerTest {
subscribe = { events += "subscribe:$it" },
unsubscribe = { events += "unsubscribe" },
)
controller.updateAppForeground(true)
controller.updateAvailability(true, true, "u4pruydq")
controller.activate()
controller.reveal()