From 05bbe21f8792027654268a7ca80a3f746d368d17 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 03:51:43 +0000 Subject: [PATCH] refactor: replace Amethyst.instance reads in screens with app services and host ports - AppServices gains appStores (the preference DataStores) and torBootstrapped; the chess view-model factory, search history and the relay-group Tor banner read them instead of Amethyst.instance. - The relay-group banner reads Tor routing from the host; ClinkOfferPayer takes the host's MoneyOpRelayRouting; the napplet manifest lookup reads LocalCache directly. - AccountViewModelHost gains hasBackedUpKeys/setHasBackedUpKeys for the backup nudge, over LocalPreferences on Android. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/napplet/NappletManifestLookup.kt | 4 ++-- .../amethyst/service/ClinkOfferPayer.kt | 5 +++-- .../note/creators/invoice/ClinkOfferPreview.kt | 2 +- .../amethyst/ui/note/types/Chess.kt | 3 ++- .../amethyst/ui/platform/AndroidAppServices.kt | 9 +++++++++ .../loggedIn/AndroidAccountViewModelHost.kt | 7 +++++++ .../relayGroup/RelayGroupChannelListScreen.kt | 9 ++++----- .../loggedIn/chess/ChessDismissedGamesData.kt | 5 ++--- .../screen/loggedIn/chess/ChessGameScreen.kt | 3 ++- .../screen/loggedIn/chess/ChessLobbyScreen.kt | 3 ++- .../loggedIn/chess/ChessViewModelFactory.kt | 4 +++- .../screen/loggedIn/home/NewChessGameButton.kt | 3 ++- .../loggedIn/keyBackup/BackupKeysNudge.kt | 18 +++++++++--------- .../profile/payment/SendPaymentScreen.kt | 2 +- .../ui/screen/loggedIn/search/SearchScreen.kt | 5 +++-- .../amethyst/commons/service/AppServices.kt | 8 ++++++++ .../commons/viewmodels/AccountViewModelHost.kt | 13 +++++++++++++ 17 files changed, 73 insertions(+), 30 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt index 067661e93e..2e97d994fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.napplet -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent @@ -38,7 +38,7 @@ fun resolveNappletMeta( untitled: String, ): Pair { val events = - Amethyst.instance.cache + LocalCache .filter( Filter( kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND, RootSiteEvent.KIND, NamedSiteEvent.KIND), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt index 711c20d74e..d317102733 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.quartz.experimental.clink.client.OfferClient import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent import com.vitorpamplona.quartz.experimental.clink.offers.OfferResponse @@ -57,6 +57,7 @@ object ClinkOfferPayer { */ suspend fun requestInvoice( account: Account, + moneyOpRelays: MoneyOpRelayRouting, offer: NOffer, amountSats: Long? = null, timeoutMs: Long = DEFAULT_TIMEOUT_MS, @@ -103,7 +104,7 @@ object ClinkOfferPayer { // pushing the payment through Tor (and failing on services that block Tor exits) even when // the user disabled Tor for money operations. The subscribe() below triggers a reconnect, and // BasicRelayClient rebuilds any socket left on the now-wrong (Tor) transport onto clearnet. - val torState = Amethyst.instance.torEvaluatorFlow + val torState = moneyOpRelays torState.registerMoneyOpRelays(relays) account.client.subscribe(subId, filters, listener) try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt index 9fbe8d74f1..fb26066669 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt @@ -182,7 +182,7 @@ fun ClinkOfferPreview( ) { val amount = if (amountRequired) amountInput.toLongOrNull() else useOffer.price - val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, useOffer, amountSats = amount) + val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, accountViewModel.host.moneyOpRelays, useOffer, amountSats = amount) val bolt11 = response?.bolt11 val movedTo = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index d1afd49999..4648940283 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory @@ -127,7 +128,7 @@ fun RenderLiveChessChallenge( val chessViewModel: ChessViewModel = viewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) val isOpenChallenge = event.opponentPubkey() == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt index ae647dadc1..9b56a1e0a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.AppServices import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder @@ -33,6 +34,8 @@ import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * [AppServices] over the main process's app modules. Every member is a getter, so installing @@ -52,6 +55,12 @@ object AndroidAppServices : AppServices { override val torSettings: TorSettingsFlow get() = Amethyst.instance.torPrefs.value + override val torBootstrapped: Flow get() = + Amethyst.instance.torManager.status + .map { it.isFullyBootstrapped } + + override val appStores: AppPreferenceStores get() = Amethyst.instance.appStores + override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index ddd888b277..e1c361343c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -86,6 +86,13 @@ class AndroidAccountViewModelHost( override val scheduledPostStore: ScheduledPostStore get() = modules.scheduledPostStore + override suspend fun hasBackedUpKeys(npub: String): StateFlow = LocalPreferences.hasBackedUpKeys(npub) + + override suspend fun setHasBackedUpKeys( + npub: String, + value: Boolean, + ) = LocalPreferences.setHasBackedUpKeys(value, npub) + override suspend fun anonymizeVoice( input: Path, presetName: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index d227ec69ec..bb46aad431 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.buzz.ui.PresenceDot import com.vitorpamplona.amethyst.commons.chats.publicChannels.relayGroup.newestTimelineNote import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -107,6 +106,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListViewModel @@ -366,7 +366,7 @@ fun RelayGroupChannelListScreen( // was never consulted, and neither was whether this relay is Tor-routed at all (Tor being *on* // doesn't mean this relay goes through it; the per-role presets decide). val torEvaluation = - Amethyst.instance.torEvaluatorFlow.flow + accountViewModel.host.torRelayEvaluation .collectAsStateWithLifecycle() // The same predicate the relay pool itself dials with, so the banner can't claim Tor for a relay // the app is reaching over clearnet (onion / localhost / trusted-off-Tor are all folded in here). @@ -374,9 +374,8 @@ fun RelayGroupChannelListScreen( // While Tor is bootstrapping every Tor-routed relay is silent — that's Tor's own failure (and its // own dialog), so don't let it read as "this relay blocks Tor exits". - val torStatus by Amethyst.instance.torManager.status - .collectAsStateWithLifecycle() - val torIsUp = torStatus.isFullyBootstrapped + val torIsUp by LocalAppServices.current.torBootstrapped + .collectAsStateWithLifecycle(false) // The offer adds the relay to the kind-10089 Trusted list, which only moves it to clearnet while // trusted relays are *off* Tor. Under the Small-Payloads / Full-Privacy presets they are on Tor, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt index a0cba559e4..8aa803bd54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores /** * Where Android keeps the dismissed-chess-games store. @@ -32,5 +32,4 @@ import com.vitorpamplona.amethyst.Amethyst * data over is not worth the code. Anyone who had dismissed a game sees it once * more and dismisses it again. */ -internal val chessDismissedGamesData: DataStore - get() = Amethyst.instance.appStores.getDataStore("chess_dismissed_games_v2") +internal fun chessDismissedGamesData(stores: AppPreferenceStores): DataStore = stores.getDataStore("chess_dismissed_games_v2") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt index 183dfb2e18..11af042705 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.chess_loading_game import com.vitorpamplona.amethyst.commons.resources.go_back import com.vitorpamplona.amethyst.commons.resources.relay_settings import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -104,7 +105,7 @@ fun ChessGameScreen( viewModel( viewModelStoreOwner = activity, key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) val activeGames by chessViewModel.activeGames.collectAsState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index a41afb483a..89e93c15d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.resources.route_chess import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription @@ -102,7 +103,7 @@ fun ChessLobbyScreen( viewModel( viewModelStoreOwner = activity, key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) // Subscribe to chess events when screen is visible diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt index ab7d7a646e..ab27e660cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel @@ -34,6 +35,7 @@ import kotlin.reflect.KClass */ class ChessViewModelFactory( private val account: Account, + private val appStores: AppPreferenceStores, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -47,7 +49,7 @@ class ChessViewModelFactory( fetcher = AndroidRelayFetcher(account), metadataProvider = AndroidMetadataProvider(), pollingConfig = ChessPollingDefaults.android, - dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData), + dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData(appStores)), ) as T } throw IllegalArgumentException("Unknown ViewModel class") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt index 22c74905d0..a0e91364df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.nip64Chess.ui.NewChessGameDialog import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_chess_game import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -56,7 +57,7 @@ fun NewChessGameButton( val chessViewModel: ChessViewModel = viewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) FloatingActionButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt index 7dd5fce323..0054503041 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -58,7 +57,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip19Bech32.toNpub -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** @@ -66,7 +65,7 @@ import kotlinx.coroutines.launch * generated accounts that haven't saved their secret key yet. It never blocks * navigation: the user either backs up (navigates to [Route.AccountBackup]) or * confirms they already saved the key. Both actions flip the per-account - * [LocalPreferences.setHasBackedUpKeys] flag so the nudge stops appearing. + * backed-up flag so the nudge stops appearing. */ @Composable fun BackupKeysNudge( @@ -81,19 +80,20 @@ fun BackupKeysNudge( // Seed the reactive flag off a background read. Rendering only proceeds once the // flow resolves, so the observing composable never conditionally calls hooks. - val flow by produceState?>(initialValue = null, key1 = npub) { - value = LocalPreferences.hasBackedUpKeys(npub) + val flow by produceState?>(initialValue = null, key1 = npub) { + value = accountViewModel.host.hasBackedUpKeys(npub) } flow?.let { stateFlow -> - WatchBackupKeysNudge(stateFlow, npub, nav, modifier) + WatchBackupKeysNudge(stateFlow, npub, accountViewModel, nav, modifier) } } @Composable private fun WatchBackupKeysNudge( - stateFlow: MutableStateFlow, + stateFlow: StateFlow, npub: String, + accountViewModel: AccountViewModel, nav: INav, modifier: Modifier, ) { @@ -106,11 +106,11 @@ private fun WatchBackupKeysNudge( onBackupNow = { // Best-effort: opening the backup screen counts as backing up so the // nudge doesn't linger after the user follows through. - scope.launch { LocalPreferences.setHasBackedUpKeys(true, npub) } + scope.launch { accountViewModel.host.setHasBackedUpKeys(npub, true) } nav.nav(Route.AccountBackup) }, onAlreadySaved = { - scope.launch { LocalPreferences.setHasBackedUpKeys(true, npub) } + scope.launch { accountViewModel.host.setHasBackedUpKeys(npub, true) } }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index f30f5a1bbe..c695660415 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -488,7 +488,7 @@ private fun SendPaymentLoaded( amount: Long?, followMoved: Boolean, ) { - val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, useOffer, amountSats = amount) + val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, accountViewModel.host.moneyOpRelays, useOffer, amountSats = amount) val bolt11 = response?.bolt11 val movedTo = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 1417b7553f..34f42ff0fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -79,7 +79,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -130,6 +129,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow @@ -168,7 +168,8 @@ fun SearchScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val historyStorage = remember { DataStoreSearchHistoryStorage(Amethyst.instance.appStores.getDataStore(DataStoreSearchHistoryStorage.FILE_NAME)) } + val appStores = LocalAppServices.current.appStores + val historyStorage = remember { DataStoreSearchHistoryStorage(appStores.getDataStore(DataStoreSearchHistoryStorage.FILE_NAME)) } val searchBarViewModel: SearchBarViewModel = viewModel( // Keyed on the seed: navigating from one screen's search button to another's has to diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt index 2f113e23a5..542bd89360 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -25,10 +25,12 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver +import kotlinx.coroutines.flow.Flow /** * App-wide stores that outlive any one account and that screens read directly: the favorites @@ -53,6 +55,12 @@ interface AppServices { val torSettings: TorSettingsFlow + /** Whether the embedded Tor service has fully bootstrapped, so Tor-routed relays can answer. */ + val torBootstrapped: Flow + + /** The app's preference files, one DataStore per name. */ + val appStores: AppPreferenceStores + /** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */ val namecoinResolver: NamecoinNameResolver diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 6eb5d1830c..4e6967c763 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import okio.Path @@ -93,6 +94,18 @@ interface AccountViewModelHost { /** A new on-device writing assistant, or null where the platform has none. */ fun createWritingAssistant(): WritingAssistant? = null + /** + * Whether the account [npub] has saved its secret key, as a live flag. Absent reads as true, + * so a platform that does not track it never shows the backup nudge. + */ + suspend fun hasBackedUpKeys(npub: String): StateFlow = MutableStateFlow(true) + + /** Records whether the account [npub] has saved its secret key. */ + suspend fun setHasBackedUpKeys( + npub: String, + value: Boolean, + ) = Unit + /** Public keys of every account saved on this device. */ val savedAccounts: Flow>