From d8911ef69c428d7d9f1e666da2fae4db789c515c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 3 Mar 2026 16:42:45 -0500 Subject: [PATCH] Account State is more than just a view model. --- .../com/vitorpamplona/amethyst/AppModules.kt | 11 +++ .../vitorpamplona/amethyst/ui/MainActivity.kt | 11 +-- .../amethyst/ui/components/WindowUtils.kt | 9 ++- .../amethyst/ui/navigation/AppNavigation.kt | 20 +++--- .../drawer/AccountSwitchBottomSheet.kt | 22 +++--- .../amethyst/ui/screen/AccountScreen.kt | 18 ++--- ...eViewModel.kt => AccountSessionManager.kt} | 68 ++++++++++--------- .../AccountSwitcherAndLeftDrawerLayout.kt | 6 +- .../ui/screen/loggedIn/LoggedInPage.kt | 6 +- .../ui/screen/loggedOff/AddAccountDialog.kt | 6 +- .../screen/loggedOff/LoginOrSignupScreen.kt | 8 +-- .../ui/screen/loggedOff/login/LoginScreen.kt | 15 ++-- .../screen/loggedOff/login/LoginViewModel.kt | 18 +++-- .../loggedOff/login/OpenURIIfNotLoggedIn.kt | 4 +- .../screen/loggedOff/signup/SignUpScreen.kt | 15 ++-- .../loggedOff/signup/SignUpViewModel.kt | 18 +++-- 16 files changed, 144 insertions(+), 111 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/{AccountStateViewModel.kt => AccountSessionManager.kt} (86%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index f9ac87aaf..559934407 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.Notify import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.UiSettingsState import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @@ -231,6 +232,15 @@ class AppModules( client = client, ) + val sessionManager = + AccountSessionManager( + accountsCache = accountsCache, + nip05Client = nip05Client, + client = client, + localPreferences = LocalPreferences, + scope = applicationIOScope, + ) + // Organizes cache clearing val trimmingService = MemoryTrimmingService(cache) @@ -274,6 +284,7 @@ class AppModules( applicationIOScope.launch { // loads main account quickly. LocalPreferences.loadAccountConfigFromEncryptedStorage() + sessionManager.loginWithDefaultAccountIfLoggedOff() } // forces initialization of uiPrefs in the main thread to avoid blinking themes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 4ac789e64..9bc0d0057 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -26,8 +26,6 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.model.Account @@ -38,7 +36,6 @@ import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.AccountScreen -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser @@ -68,13 +65,7 @@ class MainActivity : AppCompatActivity() { setContent { StringResSetup() AmethystTheme { - val accountStateViewModel: AccountStateViewModel = viewModel() - - LaunchedEffect(key1 = Unit) { - accountStateViewModel.loginWithDefaultAccountIfLoggedOff() - } - - AccountScreen(accountStateViewModel) + AccountScreen(Amethyst.instance.sessionManager) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt index 07c0ba06c..07522149c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt @@ -46,7 +46,7 @@ private tailrec fun Context.getActivityWindow(): Window? = } @Composable -fun getActivity(): Activity? = LocalContext.current.getActivity() +fun getActivity(): Activity = LocalContext.current.getActivity() tailrec fun Context.getActivity(): ComponentActivity = when (this) { @@ -55,6 +55,13 @@ tailrec fun Context.getActivity(): ComponentActivity = else -> throw IllegalStateException("Requires a ComponentActivity to run") } +fun Context.getActivityOrNull(): ComponentActivity? = + when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null + } + @Composable fun amethystApp(): Amethyst = LocalContext.current.asAmethystApp() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 632cc1881..df3db2891 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.BookmarkListScreen @@ -128,11 +128,11 @@ import java.net.URI @Composable fun AppNavigation( accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { val nav = rememberNav() - AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountStateViewModel, nav) { + AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) { NavHost( navController = nav.controller, startDestination = Route.Home, @@ -320,7 +320,7 @@ fun AppNavigation( } } - NavigateIfIntentRequested(nav, accountViewModel, accountStateViewModel) + NavigateIfIntentRequested(nav, accountViewModel, accountSessionManager) DisplayErrorMessages(accountViewModel.toastManager, accountViewModel, nav) DisplayNotifyMessages(accountViewModel, nav) @@ -332,7 +332,7 @@ fun AppNavigation( private fun NavigateIfIntentRequested( nav: Nav, accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { accountViewModel.firstRoute?.let { newRoute -> accountViewModel.firstRoute = null @@ -386,8 +386,8 @@ private fun NavigateIfIntentRequested( if (actionableNextPage != null) { actionableNextPage?.let { nextRoute -> val npub = runCatching { URI(intentNextPage.removePrefix("nostr:")).findParameterValue("account") }.getOrNull() - if (npub != null && accountStateViewModel.currentAccountNPub() != npub) { - accountStateViewModel.checkAndSwitchUserSync(npub, nextRoute) + if (npub != null && accountSessionManager.currentAccountNPub() != npub) { + accountSessionManager.checkAndSwitchUserSync(npub, nextRoute) } else { val currentRoute = getRouteWithArguments(nextRoute::class, nav.controller) if (!isSameRoute(currentRoute, nextRoute)) { @@ -442,8 +442,8 @@ private fun NavigateIfIntentRequested( if (newPage != null) { scope.launch { val npub = runCatching { URI(uri.removePrefix("nostr:")).findParameterValue("account") }.getOrNull() - if (npub != null && accountStateViewModel.currentAccountNPub() != npub) { - accountStateViewModel.checkAndSwitchUserSync(npub, newPage) + if (npub != null && accountSessionManager.currentAccountNPub() != npub) { + accountSessionManager.checkAndSwitchUserSync(npub, newPage) } else { val currentRoute = getRouteWithArguments(newPage::class, nav.controller) if (!isSameRoute(currentRoute, newPage)) { @@ -474,7 +474,7 @@ private fun NavigateIfIntentRequested( } if (newAccount != null) { - AddAccountDialog(newAccount, accountStateViewModel) { newAccount = null } + AddAccountDialog(newAccount, accountSessionManager) { newAccount = null } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index 1d27fb432..4cbd74bfe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.note.toShortDisplay -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog import com.vitorpamplona.amethyst.ui.stringRes @@ -78,7 +78,7 @@ import kotlinx.coroutines.launch @Composable fun AccountSwitchBottomSheet( accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { var popupExpanded by remember { mutableStateOf(false) } val scrollState = rememberScrollState() @@ -94,7 +94,7 @@ fun AccountSwitchBottomSheet( ) { Text(stringRes(R.string.account_switch_select_account), fontWeight = FontWeight.Bold) } - DisplayAllAccounts(accountViewModel, accountStateViewModel) + DisplayAllAccounts(accountViewModel, accountSessionManager) Row( modifier = Modifier @@ -110,24 +110,24 @@ fun AccountSwitchBottomSheet( } if (popupExpanded) { - AddAccountDialog(null, accountStateViewModel) { popupExpanded = false } + AddAccountDialog(null, accountSessionManager) { popupExpanded = false } } } @Composable private fun DisplayAllAccounts( accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { val accounts by LocalPreferences.accountsFlow().collectAsStateWithLifecycle() - accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) } + accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountSessionManager) } } @Composable fun DisplayAccount( acc: AccountInfo, accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { var baseUser by remember(acc) { mutableStateOf( @@ -153,7 +153,7 @@ fun DisplayAccount( modifier = Modifier .fillMaxWidth() - .clickable { accountStateViewModel.switchUser(acc) } + .clickable { accountSessionManager.switchUser(acc) } .padding(16.dp, 16.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -179,7 +179,7 @@ fun DisplayAccount( } } - LogoutButton(acc, accountStateViewModel) + LogoutButton(acc, accountSessionManager) } } } @@ -247,7 +247,7 @@ private fun AccountName( @Composable private fun LogoutButton( acc: AccountInfo, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { var logoutDialog by remember { mutableStateOf(false) } if (logoutDialog) { @@ -259,7 +259,7 @@ private fun LogoutButton( TextButton( onClick = { logoutDialog = false - accountStateViewModel.logOff(acc) + accountSessionManager.logOff(acc) }, ) { Text(text = stringRes(R.string.log_out)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 8207b3880..4df5ab134 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -41,14 +41,14 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.utils.Log @Composable -fun AccountScreen(accountStateViewModel: AccountStateViewModel) { +fun AccountScreen(accountSessionManager: AccountSessionManager) { // Pauses relay services when the app pauses ManageRelayServices() ManageWebOkHttp() - val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle() + val accountState by accountSessionManager.accountContent.collectAsStateWithLifecycle() - Log.d("ActivityLifecycle", "AccountScreen $accountState $accountStateViewModel") + Log.d("ActivityLifecycle", "AccountScreen $accountState $accountSessionManager") Crossfade( targetState = accountState, @@ -56,8 +56,8 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel) { ) { state -> when (state) { is AccountState.Loading -> LoadingSetup() - is AccountState.LoggedOff -> LoggedOffSetup(accountStateViewModel) - is AccountState.LoggedIn -> LoggedInSetup(state, accountStateViewModel) + is AccountState.LoggedOff -> LoggedOffSetup(accountSessionManager) + is AccountState.LoggedIn -> LoggedInSetup(state, accountSessionManager) } } } @@ -94,25 +94,25 @@ fun LoadingSetup() { } @Composable -fun LoggedOffSetup(accountStateViewModel: AccountStateViewModel) { +fun LoggedOffSetup(accountSessionManager: AccountSessionManager) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { - LoginOrSignupScreen(null, accountStateViewModel, isFirstLogin = true) + LoginOrSignupScreen(null, accountSessionManager, isFirstLogin = true) } } @Composable fun LoggedInSetup( state: AccountState.LoggedIn, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { SetAccountCentricViewModelStore(state) { LoggedInPage( account = state.account, route = state.route, - accountStateViewModel = accountStateViewModel, + accountSessionManager = accountSessionManager, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt index 65b87c9b7..a518e06ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt @@ -21,10 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.AccountInfo -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultChannels @@ -33,14 +30,17 @@ import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.DefaultNIP65List import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet import com.vitorpamplona.amethyst.model.DefaultSearchRelayList +import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag +import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @@ -63,9 +63,8 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -77,21 +76,27 @@ import java.util.regex.Pattern val EMAIL_PATTERN: Pattern = Pattern.compile(".+@.+\\.[a-z]+") @Stable -class AccountStateViewModel : ViewModel() { +class AccountSessionManager( + val accountsCache: AccountCacheState, + val nip05Client: Nip05Client, + val client: INostrClient, + val localPreferences: LocalPreferences, + val scope: CoroutineScope, +) { private val _accountContent = MutableStateFlow(AccountState.Loading) val accountContent = _accountContent.asStateFlow() fun loginWithDefaultAccountIfLoggedOff() { // pulls account from storage. if (_accountContent.value !is AccountState.LoggedIn) { - viewModelScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { loginWithDefaultAccount() } } } private suspend fun loginWithDefaultAccount(route: Route? = null) { - val accountSettings = LocalPreferences.loadAccountConfigFromEncryptedStorage() + val accountSettings = localPreferences.loadAccountConfigFromEncryptedStorage() if (accountSettings != null) { startUI(accountSettings, route) @@ -155,7 +160,7 @@ class AccountStateViewModel : ViewModel() { ) } - LocalPreferences.setDefaultAccount(accountSettings) + localPreferences.setDefaultAccount(accountSettings) startUI(accountSettings) } @@ -164,7 +169,7 @@ class AccountStateViewModel : ViewModel() { accountSettings: AccountSettings, route: Route? = null, ) { - val account = Amethyst.instance.accountsCache.loadAccount(accountSettings) + val account = accountsCache.loadAccount(accountSettings) _accountContent.update { AccountState.LoggedIn(account, route) } @@ -187,7 +192,7 @@ class AccountStateViewModel : ViewModel() { packageName: String = "", onError: (String?) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { if (key.startsWith("ncryptsec")) { val newKey = try { @@ -214,7 +219,7 @@ class AccountStateViewModel : ViewModel() { onError("Could not parse nip05 address: $nip05") } else { try { - val pubkeyInfo = Amethyst.instance.nip05Client.get(nip05) + val pubkeyInfo = nip05Client.get(nip05) if (pubkeyInfo == null) { onError("User not found in the nip05 server: $nip05") } else { @@ -238,7 +243,7 @@ class AccountStateViewModel : ViewModel() { packageName: String = "", onError: (String) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError) } } @@ -263,7 +268,7 @@ class AccountStateViewModel : ViewModel() { } fun newKey(name: String? = null) { - viewModelScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { if (_accountContent.value is AccountState.LoggedIn) { prepareLogoutOrSwitch() } @@ -272,22 +277,21 @@ class AccountStateViewModel : ViewModel() { val accountSettings = createNewAccount(name) - LocalPreferences.setDefaultAccount(accountSettings) + localPreferences.setDefaultAccount(accountSettings) startUI(accountSettings) - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { delay(2000) // waits for the new user to connect to the new relays. val toPost = accountSettings.backupNIP65RelayList?.writeRelaysNorm()?.toSet() ?: DefaultNIP65RelaySet - accountSettings.backupUserMetadata?.let { Amethyst.instance.client.send(it, toPost) } - accountSettings.backupContactList?.let { Amethyst.instance.client.send(it, toPost) } - accountSettings.backupNIP65RelayList?.let { Amethyst.instance.client.send(it, toPost) } - accountSettings.backupDMRelayList?.let { Amethyst.instance.client.send(it, toPost) } - accountSettings.backupSearchRelayList?.let { Amethyst.instance.client.send(it, toPost) } - accountSettings.backupIndexRelayList?.let { Amethyst.instance.client.send(it, toPost) } + accountSettings.backupUserMetadata?.let { client.send(it, toPost) } + accountSettings.backupContactList?.let { client.send(it, toPost) } + accountSettings.backupNIP65RelayList?.let { client.send(it, toPost) } + accountSettings.backupDMRelayList?.let { client.send(it, toPost) } + accountSettings.backupSearchRelayList?.let { client.send(it, toPost) } + accountSettings.backupIndexRelayList?.let { client.send(it, toPost) } } } } @@ -315,7 +319,7 @@ class AccountStateViewModel : ViewModel() { } fun switchUser(accountInfo: AccountInfo) { - viewModelScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { switchUserSync(accountInfo) } } @@ -324,8 +328,8 @@ class AccountStateViewModel : ViewModel() { npub: String, route: Route, ): Boolean { - if (npub != LocalPreferences.currentAccount()) { - val account = LocalPreferences.allSavedAccounts().firstOrNull { it.npub == npub } + if (npub != localPreferences.currentAccount()) { + val account = localPreferences.allSavedAccounts().firstOrNull { it.npub == npub } if (account != null) { switchUserSync(account, route) return true @@ -339,7 +343,7 @@ class AccountStateViewModel : ViewModel() { route: Route? = null, ) { prepareLogoutOrSwitch() - LocalPreferences.switchToAccount(accountInfo) + localPreferences.switchToAccount(accountInfo) loginWithDefaultAccount(route) } @@ -357,17 +361,17 @@ class AccountStateViewModel : ViewModel() { } fun logOff(accountInfo: AccountInfo) { - viewModelScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { if (accountInfo.npub == currentAccountNPub()) { // log off and relogin with the 0 account prepareLogoutOrSwitch() - LocalPreferences.deleteAccount(accountInfo) - Amethyst.instance.accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) + localPreferences.deleteAccount(accountInfo) + accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) loginWithDefaultAccount() } else { // delete without switching logins - LocalPreferences.deleteAccount(accountInfo) - Amethyst.instance.accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) + localPreferences.deleteAccount(accountInfo) + accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index 4c25c4392..84ec07af7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -40,14 +40,14 @@ import androidx.compose.ui.platform.LocalConfiguration import com.vitorpamplona.amethyst.ui.navigation.drawer.AccountSwitchBottomSheet import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerContent import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountSwitcherAndLeftDrawerLayout( accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, nav: INav, content: @Composable () -> Unit, ) { @@ -106,7 +106,7 @@ fun AccountSwitcherAndLeftDrawerLayout( ) { AccountSwitchBottomSheet( accountViewModel = accountViewModel, - accountStateViewModel = accountStateViewModel, + accountSessionManager = accountSessionManager, ) } } 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 63903ed3c..655a60487 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 @@ -46,7 +46,7 @@ import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayA import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription @@ -59,7 +59,7 @@ import kotlinx.coroutines.launch fun LoggedInPage( account: Account, route: Route?, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, ) { val accountViewModel: AccountViewModel = viewModel( @@ -99,7 +99,7 @@ fun LoggedInPage( AppNavigation( accountViewModel = accountViewModel, - accountStateViewModel = accountStateViewModel, + accountSessionManager = accountSessionManager, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/AddAccountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/AddAccountDialog.kt index 9f6986625..d79431f63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/AddAccountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/AddAccountDialog.kt @@ -35,14 +35,14 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.stringRes @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddAccountDialog( newAccountKey: String?, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, onClose: () -> Unit, ) { Dialog( @@ -51,7 +51,7 @@ fun AddAccountDialog( ) { Surface(modifier = Modifier.fillMaxSize()) { Box { - LoginOrSignupScreen(newAccountKey, accountStateViewModel, isFirstLogin = false) + LoginOrSignupScreen(newAccountKey, accountSessionManager, isFirstLogin = false) TopAppBar( title = { Text(text = stringRes(R.string.account_switch_add_account_dialog_title)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt index edea2959c..9dd0c7090 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt @@ -26,14 +26,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginPage import com.vitorpamplona.amethyst.ui.screen.loggedOff.signup.SignUpPage @Composable fun LoginOrSignupScreen( newAccountKey: String?, - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, isFirstLogin: Boolean, ) { var wantsNewUser by remember { @@ -42,11 +42,11 @@ fun LoginOrSignupScreen( Crossfade(wantsNewUser, label = "LoginOrSignupScreen") { if (it) { - SignUpPage(accountStateViewModel) { + SignUpPage(accountSessionManager) { wantsNewUser = false } } else { - LoginPage(accountStateViewModel, isFirstLogin, newAccountKey) { + LoginPage(accountSessionManager, isFirstLogin, newAccountKey) { wantsNewUser = true } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt index 7a6269393..d671fe53e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt @@ -78,7 +78,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Amethyst import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedOff.AcceptTerms import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup import com.vitorpamplona.amethyst.ui.stringRes @@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip55AndroidSigner.client.isExternalSignerInstalled import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -94,24 +95,26 @@ import kotlinx.coroutines.launch @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @Composable fun LoginPagePreview() { - val accountViewModel: AccountStateViewModel = viewModel() + val loginViewModel: LoginViewModel = viewModel() + loginViewModel.init(TorSettingsFlow()) ThemeComparisonRow( toPreview = { - LoginPage(accountViewModel, true) {} + LoginPage(loginViewModel) {} }, ) } @Composable fun LoginPage( - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, isFirstLogin: Boolean, newAccountKey: String? = null, onWantsToLogin: () -> Unit, ) { val loginViewModel: LoginViewModel = viewModel() - loginViewModel.init(accountStateViewModel) + loginViewModel.init(accountSessionManager) + loginViewModel.init(Amethyst.instance.torPrefs.value) LaunchedEffect(loginViewModel, isFirstLogin, newAccountKey) { loginViewModel.load(isFirstLogin, newAccountKey) @@ -180,7 +183,7 @@ fun LoginPage( Spacer(modifier = Modifier.height(10.dp)) TorSettingsSetup( - torSettingsFlow = Amethyst.instance.torPrefs.value, + torSettingsFlow = loginViewModel.torSettings, onError = { scope.launch { Toast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt index 919783de9..35d4bfff5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt @@ -28,11 +28,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow @Stable class LoginViewModel : ViewModel() { - lateinit var accountStateViewModel: AccountStateViewModel + lateinit var accountSessionManager: AccountSessionManager + lateinit var torSettings: TorSettingsFlow val errorManager = LoginErrorManager() @@ -52,8 +54,12 @@ class LoginViewModel : ViewModel() { var isFirstLogin by mutableStateOf(false) - fun init(accountStateViewModel: AccountStateViewModel) { - this.accountStateViewModel = accountStateViewModel + fun init(accountSessionManager: AccountSessionManager) { + this.accountSessionManager = accountSessionManager + } + + fun init(torSettings: TorSettingsFlow) { + this.torSettings = torSettings } fun load( @@ -132,7 +138,7 @@ class LoginViewModel : ViewModel() { fun login() { if (checkCanLogin()) { processingLogin = true - accountStateViewModel.login( + accountSessionManager.login( key = key.text, password = password.text, transientAccount = isTemporary, @@ -150,7 +156,7 @@ class LoginViewModel : ViewModel() { fun loginWithExternalSigner(packageName: String) { if (checkCanLogin()) { processingLogin = true - accountStateViewModel.login( + accountSessionManager.login( key = key.text, transientAccount = isTemporary, loginWithExternalSigner = true, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt index 018ca4e4c..5c1748cca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.core.util.Consumer import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.components.getActivity +import com.vitorpamplona.amethyst.ui.components.getActivityOrNull import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import kotlinx.coroutines.delay @@ -42,7 +42,7 @@ import kotlinx.coroutines.launch @Composable fun OpenURIIfNotLoggedIn(onNewNIP19: suspend (String) -> Unit) { val context = LocalContext.current - val activity = context.getActivity() + val activity = context.getActivityOrNull() ?: return val scope = rememberCoroutineScope() var currentIntentNextPage by remember { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt index 4e741e275..62616886c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Amethyst import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedOff.AcceptTerms import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager @@ -63,27 +63,30 @@ import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import kotlinx.coroutines.launch @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @Composable fun SignUpPagePreview() { - val accountViewModel: AccountStateViewModel = viewModel() + val signUpViewModel: SignUpViewModel = viewModel() + signUpViewModel.init(TorSettingsFlow()) ThemeComparisonRow( toPreview = { - SignUpPage(accountViewModel) {} + SignUpPage(signUpViewModel) {} }, ) } @Composable fun SignUpPage( - accountStateViewModel: AccountStateViewModel, + accountSessionManager: AccountSessionManager, onWantsToLogin: () -> Unit, ) { val signUpViewModel: SignUpViewModel = viewModel() - signUpViewModel.init(accountStateViewModel) + signUpViewModel.init(accountSessionManager) + signUpViewModel.init(Amethyst.instance.torPrefs.value) SignUpPage(signUpViewModel, onWantsToLogin) } @@ -171,7 +174,7 @@ fun SignUpPage( Spacer(modifier = Modifier.height(10.dp)) TorSettingsSetup( - torSettingsFlow = Amethyst.instance.torPrefs.value, + torSettingsFlow = signUpViewModel.torSettings, onError = { scope.launch { Toast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt index eb4b0c2d0..6f87efac7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt @@ -20,17 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow +@Stable class SignUpViewModel : ViewModel() { - lateinit var accountStateViewModel: AccountStateViewModel + lateinit var accountSessionManager: AccountSessionManager + lateinit var torSettings: TorSettingsFlow val errorManager = LoginErrorManager() @@ -39,8 +43,12 @@ class SignUpViewModel : ViewModel() { var acceptedTerms by mutableStateOf(false) var termsAcceptanceIsRequiredError by mutableStateOf(false) - fun init(accountStateViewModel: AccountStateViewModel) { - this.accountStateViewModel = accountStateViewModel + fun init(accountSessionManager: AccountSessionManager) { + this.accountSessionManager = accountSessionManager + } + + fun init(torSettings: TorSettingsFlow) { + this.torSettings = torSettings } fun updateDisplayName(value: TextFieldValue) { @@ -73,7 +81,7 @@ class SignUpViewModel : ViewModel() { fun signup() { if (checkCanSignup()) { - accountStateViewModel.newKey(displayName.text) + accountSessionManager.newKey(displayName.text) } } }