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 * Merely rendering the mesh timeline must not open a building-precision Nostr
* subscription. A subscription is eligible only after an explicit reveal and * 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 @MainThread
class NearbyNotesController internal constructor( class NearbyNotesController internal constructor(
@ -25,6 +25,7 @@ class NearbyNotesController internal constructor(
private var activeHolders = 0 private var activeHolders = 0
private var locationEnabled = false private var locationEnabled = false
private var locationAuthorized = false private var locationAuthorized = false
private var appForeground = false
private var buildingGeohash: String? = null private var buildingGeohash: String? = null
private var subscribedGeohash: String? = null private var subscribedGeohash: String? = null
@ -50,6 +51,12 @@ class NearbyNotesController internal constructor(
reconcileSubscription() 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. * Updates the privacy-sensitive inputs independently of view activation.
* Permission revocation, location disable, or loss of the building cell * Permission revocation, location disable, or loss of the building cell
@ -78,6 +85,7 @@ class NearbyNotesController internal constructor(
private fun reconcileSubscription() { private fun reconcileSubscription() {
val target = buildingGeohash.takeIf { val target = buildingGeohash.takeIf {
activeHolders > 0 && activeHolders > 0 &&
appForeground &&
_revealed.value && _revealed.value &&
locationEnabled && locationEnabled &&
locationAuthorized locationAuthorized

View File

@ -29,6 +29,10 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.zIndex 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.R import com.bitchat.android.R
import com.bitchat.android.geohash.ChannelID import com.bitchat.android.geohash.ChannelID
@ -117,6 +121,30 @@ fun ChatScreen(viewModel: ChatViewModel) {
selectedPrivatePeer == null && selectedPrivatePeer == null &&
privateChatSheetPeer == 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( DisposableEffect(
isMeshTimeline, isMeshTimeline,
locationEnabled, locationEnabled,

View File

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