From 6bcb94a658282eb143104f21150e1c10691a4705 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 19:39:23 +0000 Subject: [PATCH] feat(subscriptions): split foreground-only account loaders into AccountForegroundFilterAssembler Move AccountFollowsLoaderSubAssembler and AccountNotificationsEoseFromRandomRelaysManager out of the always-on AccountFilterAssembler and into a new AccountForegroundFilterAssembler that is mounted via LifecycleAwareKeyDataSourceSubscription. These two scan-heavy loaders now pause on ON_STOP and resume on ON_START, while the lightweight always-on account work (metadata, gift wraps, drafts, inbox-relay notifications, marmot groups) keeps running in background. https://claude.ai/code/session_01RUmRxmzUAcVXezF9PEETGz --- .../RelaySubscriptionsCoordinator.kt | 9 ++- .../account/AccountFilterAssembler.kt | 16 +---- .../AccountForegroundFilterAssembler.kt | 60 +++++++++++++++++++ ...ntForegroundFilterAssemblerSubscription.kt | 46 ++++++++++++++ .../ui/screen/loggedIn/LoggedInPage.kt | 5 ++ 5 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssemblerSubscription.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index 0859ef4b3..1694849b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler @@ -70,8 +71,11 @@ class RelaySubscriptionsCoordinator( failureTracker: RelayOfflineTracker, scope: CoroutineScope, ) { - // main one: notifications, dms and account settings - val account = AccountFilterAssembler(client, cache, authenticator, failureTracker, scope) + // main one: notifications, dms and account settings (always-on while logged in) + val account = AccountFilterAssembler(client) + + // foreground-only account loaders (follows-outbox finder, random-relay notifications) + val accountForeground = AccountForegroundFilterAssembler(client, cache, authenticator, failureTracker, scope) // always running, feed assemblers. val home = HomeFilterAssembler(client) @@ -124,6 +128,7 @@ class RelaySubscriptionsCoordinator( val all = listOf( account, + accountForeground, home, chatroomList, video, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index bbf37f88c..6b28e608b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -23,20 +23,14 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts.AccountDraftsEoseManager -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.AccountFollowsLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.marmot.MarmotGroupEventsEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromInboxRelaysManager -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker -import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus -import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to logged-in accounts. @Stable @@ -47,24 +41,20 @@ class AccountQueryState( ) /** - * This assembler loads everything eech account needs. + * Always-on account loaders: metadata, gift wraps, drafts, inbox-relay + * notifications, marmot group events. Foreground-only loaders live in + * [AccountForegroundFilterAssembler]. */ @Stable class AccountFilterAssembler( client: INostrClient, - cache: LocalCache, - authenticator: IAuthStatus, - failureTracker: RelayOfflineTracker, - scope: CoroutineScope, ) : ComposeSubscriptionManager() { val group = listOf( AccountMetadataEoseManager(client, ::allKeys), - AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys), AccountGiftWrapsEoseManager(client, ::allKeys), AccountDraftsEoseManager(client, ::allKeys), AccountNotificationsEoseFromInboxRelaysManager(client, ::allKeys), - AccountNotificationsEoseFromRandomRelaysManager(client, ::allKeys), MarmotGroupEventsEoseManager(client, ::allKeys), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt new file mode 100644 index 000000000..0678be343 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssembler.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.AccountFollowsLoaderSubAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus +import kotlinx.coroutines.CoroutineScope + +/** + * Account-scoped loaders that only need to run while the app has a screen + * in the foreground. Mounted by a lifecycle-aware subscription so they pause + * on ON_STOP and resume on ON_START. + * + * Lightweight always-on account work (metadata, gift wraps, drafts, inbox + * notifications, marmot groups) stays in [AccountFilterAssembler]. + */ +@Stable +class AccountForegroundFilterAssembler( + client: INostrClient, + cache: LocalCache, + authenticator: IAuthStatus, + failureTracker: RelayOfflineTracker, + scope: CoroutineScope, +) : ComposeSubscriptionManager() { + val group = + listOf( + AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys), + AccountNotificationsEoseFromRandomRelaysManager(client, ::allKeys), + ) + + override fun invalidateKeys() = invalidateFilters() + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.destroy() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssemblerSubscription.kt new file mode 100644 index 000000000..8938fa0ea --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountForegroundFilterAssemblerSubscription.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun AccountForegroundFilterAssemblerSubscription(accountViewModel: AccountViewModel) = + AccountForegroundFilterAssemblerSubscription( + accountViewModel, + accountViewModel.dataSources().accountForeground, + ) + +@Composable +fun AccountForegroundFilterAssemblerSubscription( + accountViewModel: AccountViewModel, + dataSource: AccountForegroundFilterAssembler, +) { + val state = + remember(accountViewModel) { + AccountQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.trustedAccounts.value) + } + + LifecycleAwareKeyDataSourceSubscription(state, dataSource) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index d78c48991..3e1843906 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager @@ -82,6 +83,10 @@ fun LoggedInPage( // Loads account information + DMs and Notifications from Relays. AccountFilterAssemblerSubscription(accountViewModel) + // Foreground-only loaders: follows-outbox finder + random-relay notifications. + // Pauses on ON_STOP, resumes on ON_START. + AccountForegroundFilterAssemblerSubscription(accountViewModel) + // Pre-loads the feed for every icon the user has pinned to the bottom bar. // Subscriptions follow the user's chosen list reactively, not the default 5. BottomBarFeedPreloaders(accountViewModel)