feat(subscriptions): make screen subscriptions lifecycle-aware

Migrates 32 call sites from KeyDataSourceSubscription to
LifecycleAwareKeyDataSourceSubscription so feed/screen REQs are
paused when the app goes to background and resumed on foreground,
saving relay bandwidth while the always-on notification service
keeps the socket open.

Adds a MutableComposeSubscriptionManager overload to
LifecycleAwareKeyDataSourceSubscription so the search bars (which
bind to a flow-driven query) can also opt in.

Skips AccountFilterAssemblerSubscription (always-on account state)
and NWCFinderFilterAssemblerSubscription (in-flight zap payments
that must complete in the background).

https://claude.ai/code/session_01RUmRxmzUAcVXezF9PEETGz
This commit is contained in:
Claude
2026-05-04 18:16:15 +00:00
parent 846b25dcd4
commit 98c0438526
33 changed files with 134 additions and 65 deletions
@@ -27,6 +27,8 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableQueryState
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -119,3 +121,70 @@ fun <T> LifecycleAwareKeyDataSourceSubscription(
}
}
}
@Composable
fun <T : MutableQueryState> LifecycleAwareKeyDataSourceSubscription(
state: T,
dataSource: MutableComposeSubscriptionManager<T>,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
DisposableEffect(state, lifecycleOwner) {
var isSubscribed = false
var pendingUnsubscribe: Job? = null
fun subscribeNow() {
pendingUnsubscribe?.cancel()
pendingUnsubscribe = null
if (!isSubscribed) {
dataSource.subscribe(state)
isSubscribed = true
}
}
fun scheduleUnsubscribe() {
if (!isSubscribed || pendingUnsubscribe != null) return
pendingUnsubscribe =
scope.launch {
delay(UNSUBSCRIBE_GRACE_MILLIS)
if (isSubscribed) {
dataSource.unsubscribe(state)
isSubscribed = false
}
pendingUnsubscribe = null
}
}
val observer =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
subscribeNow()
}
Lifecycle.Event.ON_STOP -> {
scheduleUnsubscribe()
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
subscribeNow()
}
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
pendingUnsubscribe?.cancel()
pendingUnsubscribe = null
if (isSubscribed) {
dataSource.unsubscribe(state)
isSubscribed = false
}
}
}
}