Account State is more than just a view model.

This commit is contained in:
Vitor Pamplona
2026-03-03 16:42:45 -05:00
parent 2e71401342
commit d8911ef69c
16 changed files with 144 additions and 111 deletions
@@ -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
@@ -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)
}
}
}
@@ -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()
@@ -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 }
}
}
}
@@ -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<User?>(
@@ -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))
@@ -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,
)
}
}
@@ -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>(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())
}
}
}
@@ -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,
)
}
}
@@ -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,
)
}
@@ -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))
@@ -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
}
}
@@ -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
@@ -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,
@@ -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 {
@@ -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
@@ -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)
}
}
}