feat: run EventSync in background by scoping it to AccountViewModel

Move EventSyncViewModel from a screen-scoped ViewModel to a plain class
held by AccountViewModel (activity-scoped). The sync job now runs on
AccountViewModel's viewModelScope, so it continues when the user
navigates away. The back button no longer cancels the sync.

https://claude.ai/code/session_01U8qF9mK4UBvXsXP1zNMXfX
This commit is contained in:
Claude
2026-03-13 22:42:09 +00:00
parent 441dae8e25
commit 90d7b0c678
3 changed files with 11 additions and 19 deletions
@@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSyncViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.amethyst.ui.tor.TorType
@@ -175,6 +176,7 @@ class AccountViewModel(
val toastManager = ToastManager() val toastManager = ToastManager()
val broadcastTracker = BroadcastTracker() val broadcastTracker = BroadcastTracker()
val feedStates = AccountFeedContentStates(account, viewModelScope) val feedStates = AccountFeedContentStates(account, viewModelScope)
val eventSyncViewModel = EventSyncViewModel(account, viewModelScope)
val tempManualPaymentCache = LruCache<String, List<ZapPaymentHandler.Payable>>(5) val tempManualPaymentCache = LruCache<String, List<ZapPaymentHandler.Payable>>(5)
@@ -48,7 +48,6 @@ import androidx.compose.ui.text.font.FontWeight
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
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
@@ -60,8 +59,7 @@ fun EventSyncScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val syncViewModel: EventSyncViewModel = val syncViewModel = accountViewModel.eventSyncViewModel
viewModel(factory = EventSyncViewModel.Factory(accountViewModel.account))
val syncState by syncViewModel.syncState.collectAsStateWithLifecycle() val syncState by syncViewModel.syncState.collectAsStateWithLifecycle()
val isMobileOrMetered by accountViewModel.settings.isMobileOrMeteredConnection.collectAsStateWithLifecycle() val isMobileOrMetered by accountViewModel.settings.isMobileOrMeteredConnection.collectAsStateWithLifecycle()
@@ -70,10 +68,7 @@ fun EventSyncScreen(
topBar = { topBar = {
TopBarWithBackButton( TopBarWithBackButton(
caption = stringRes(R.string.event_sync_title), caption = stringRes(R.string.event_sync_title),
popBack = { popBack = nav::popBack,
syncViewModel.cancel()
nav.popBack()
},
) )
}, },
) { padding -> ) { padding ->
@@ -21,15 +21,13 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -47,10 +45,14 @@ import kotlinx.coroutines.withTimeoutOrNull
* *
* The procedure iterates over every relay URL in LocalCache.relayHints.relayDB. * The procedure iterates over every relay URL in LocalCache.relayHints.relayDB.
* Events are deduplicated per destination to avoid redundant sends. * Events are deduplicated per destination to avoid redundant sends.
*
* Scoped to the AccountViewModel so the sync continues in the background while
* the user navigates to other screens or away from the app.
*/ */
class EventSyncViewModel( class EventSyncViewModel(
val account: Account, val account: Account,
) : ViewModel() { private val scope: CoroutineScope,
) {
sealed class SyncState { sealed class SyncState {
object Idle : SyncState() object Idle : SyncState()
@@ -81,7 +83,7 @@ class EventSyncViewModel(
fun start() { fun start() {
if (_syncState.value is SyncState.Running) return if (_syncState.value is SyncState.Running) return
syncJob = syncJob =
viewModelScope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
runSync() runSync()
} }
} }
@@ -276,11 +278,4 @@ class EventSyncViewModel(
account.client.close(subId) account.client.close(subId)
done.close() done.close()
} }
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = EventSyncViewModel(account) as T
}
} }