diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 172647486..c2c21bb24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -27,10 +27,7 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs" data class AccountInfo( val npub: String, - val hasPrivKey: Boolean, - val current: Boolean, - val displayName: String?, - val profilePicture: String? + val hasPrivKey: Boolean = false ) private object PrefKeys { @@ -38,8 +35,6 @@ private object PrefKeys { const val SAVED_ACCOUNTS = "all_saved_accounts" const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PUBKEY = "nostr_pubkey" - const val DISPLAY_NAME = "display_name" - const val PROFILE_PICTURE_URL = "profile_picture" const val FOLLOWING_CHANNELS = "following_channels" const val HIDDEN_USERS = "hidden_users" const val RELAYS = "relays" @@ -59,53 +54,73 @@ private val gson = GsonBuilder().create() object LocalPreferences { private const val comma = "," - private var currentAccount: String? - get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) - set(npub) { - val prefs = encryptedPreferences() - prefs.edit().apply { + private var _currentAccount: String? = null + + private fun currentAccount(): String? { + if (_currentAccount == null) { + _currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) + } + return _currentAccount + } + + private fun updateCurrentAccount(npub: String) { + if (_currentAccount != npub) { + _currentAccount = npub + + encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply() } + } - private val savedAccounts: List - get() = encryptedPreferences() - .getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() + private var _savedAccounts: List? = null + + private fun savedAccounts(): List { + if (_savedAccounts == null) { + _savedAccounts = encryptedPreferences() + .getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() + } + return _savedAccounts!! + } + + private fun updateSavedAccounts(accounts: List) { + if (_savedAccounts != accounts) { + _savedAccounts = accounts + + encryptedPreferences().edit().apply { + putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) + }.apply() + } + } private val prefsDirPath: String get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" private fun addAccount(npub: String) { - val accounts = savedAccounts.toMutableList() + val accounts = savedAccounts().toMutableList() if (npub !in accounts) { accounts.add(npub) + updateSavedAccounts(accounts) } - val prefs = encryptedPreferences() - prefs.edit().apply { - putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) - }.apply() } private fun setCurrentAccount(account: Account) { val npub = account.userProfile().pubkeyNpub() - currentAccount = npub + updateCurrentAccount(npub) addAccount(npub) } fun switchToAccount(npub: String) { - currentAccount = npub + updateCurrentAccount(npub) } /** * Removes the account from the app level shared preferences */ private fun removeAccount(npub: String) { - val accounts = savedAccounts.toMutableList() + val accounts = savedAccounts().toMutableList() if (accounts.remove(npub)) { - val prefs = encryptedPreferences() - prefs.edit().apply { - putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) - }.apply() + updateSavedAccounts(accounts) } } @@ -145,11 +160,11 @@ object LocalPreferences { removeAccount(npub) deleteUserPreferenceFile(npub) - if (savedAccounts.isEmpty()) { + if (savedAccounts().isEmpty()) { val appPrefs = encryptedPreferences() appPrefs.edit().clear().apply() - } else if (currentAccount == npub) { - currentAccount = savedAccounts.elementAt(0) + } else if (currentAccount() == npub) { + updateCurrentAccount(savedAccounts().elementAt(0)) } } @@ -159,17 +174,8 @@ object LocalPreferences { } fun allSavedAccounts(): List { - return savedAccounts.map { npub -> - val prefs = encryptedPreferences(npub) - val hasPrivKey = prefs.getString(PrefKeys.NOSTR_PRIVKEY, null) != null - - AccountInfo( - npub = npub, - hasPrivKey = hasPrivKey, - current = npub == currentAccount, - displayName = prefs.getString(PrefKeys.DISPLAY_NAME, null), - profilePicture = prefs.getString(PrefKeys.PROFILE_PICTURE_URL, null) - ) + return savedAccounts().map { npub -> + AccountInfo(npub = npub) } } @@ -189,13 +195,11 @@ object LocalPreferences { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) - putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName()) - putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) }.apply() } fun loadFromEncryptedStorage(): Account? { - encryptedPreferences(currentAccount).apply { + encryptedPreferences(currentAccount()).apply { val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() @@ -247,7 +251,7 @@ object LocalPreferences { val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) - return Account( + val a = Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), followingChannels, hiddenUsers, @@ -261,23 +265,25 @@ object LocalPreferences { hideBlockAlertDialog, latestContactList ) + + return a } } fun saveLastRead(route: String, timestampInSecs: Long) { - encryptedPreferences(currentAccount).edit().apply { + encryptedPreferences(currentAccount()).edit().apply { putLong(PrefKeys.LAST_READ(route), timestampInSecs) }.apply() } fun loadLastRead(route: String): Long { - encryptedPreferences(currentAccount).run { + encryptedPreferences(currentAccount()).run { return getLong(PrefKeys.LAST_READ(route), 0) } } fun migrateSingleUserPrefs() { - if (currentAccount != null) return + if (currentAccount() != null) return val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return val npub = Hex.decode(pubkey).toNpub() @@ -314,6 +320,6 @@ object LocalPreferences { encryptedPreferences().edit().clear().apply() addAccount(npub) - currentAccount = npub + updateCurrentAccount(npub) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index ceb663192..d50ce32bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -24,10 +23,8 @@ import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Logout import androidx.compose.material.icons.filled.RadioButtonChecked -import androidx.compose.material.icons.filled.Visibility import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -45,14 +42,15 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.decodePublicKey +import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage -import nostr.postr.bechToBytes -import nostr.postr.toHex @Composable fun AccountSwitchBottomSheet( @@ -82,90 +80,101 @@ fun AccountSwitchBottomSheet( accounts.forEach { acc -> val current = accountUser.pubkeyNpub() == acc.npub - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { + val baseUser = try { + LocalCache.getOrCreateUser(decodePublicKey(acc.npub).toHexKey()) + } catch (e: Exception) { + null + } + + if (baseUser != null) { + val userState by baseUser.live().metadata.observeAsState() + val user = userState?.user ?: return + Row( - modifier = Modifier - .weight(1f) - .clickable { - accountStateViewModel.switchUser(acc.npub) - }, + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Row( modifier = Modifier - .padding(16.dp, 16.dp) - .weight(1f), + .weight(1f) + .clickable { + accountStateViewModel.switchUser(acc.npub) + }, verticalAlignment = Alignment.CenterVertically ) { - Box( + Row( modifier = Modifier - .width(55.dp) - .padding(0.dp) + .padding(16.dp, 16.dp) + .weight(1f), + verticalAlignment = Alignment.CenterVertically ) { - RobohashAsyncImageProxy( - robot = acc.npub.bechToBytes("npub").toHex(), - model = ResizeImage(acc.profilePicture, 55.dp), - contentDescription = stringResource(R.string.profile_image), - modifier = Modifier - .width(55.dp) - .height(55.dp) - .clip(shape = CircleShape) - ) Box( modifier = Modifier - .size(20.dp) - .align(Alignment.TopEnd) + .width(55.dp) + .padding(0.dp) ) { - if (acc.hasPrivKey) { + RobohashAsyncImageProxy( + robot = user.pubkeyHex, + model = ResizeImage(user.profilePicture(), 55.dp), + contentDescription = stringResource(R.string.profile_image), + modifier = Modifier + .width(55.dp) + .height(55.dp) + .clip(shape = CircleShape) + )/* + Box( + modifier = Modifier + .size(20.dp) + .align(Alignment.TopEnd) + ) { + if (acc.hasPrivKey) { + Icon( + imageVector = Icons.Default.Key, + contentDescription = stringResource(R.string.account_switch_has_private_key), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } else { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = stringResource(R.string.account_switch_pubkey_only), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } + }*/ + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + val npubShortHex = acc.npub.toShortenHex() + + user.bestDisplayName()?.let { + Text(it) + } + + Text(npubShortHex) + } + Column(modifier = Modifier.width(32.dp)) { + if (current) { Icon( - imageVector = Icons.Default.Key, - contentDescription = stringResource(R.string.account_switch_has_private_key), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.primary - ) - } else { - Icon( - imageVector = Icons.Default.Visibility, - contentDescription = stringResource(R.string.account_switch_pubkey_only), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.primary + imageVector = Icons.Default.RadioButtonChecked, + contentDescription = stringResource(R.string.account_switch_active_account), + tint = MaterialTheme.colors.secondary ) } } } - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - val npubShortHex = acc.npub.toShortenHex() - - if (acc.displayName != null && acc.displayName != npubShortHex) { - Text(acc.displayName) - } - - Text(npubShortHex) - } - Column(modifier = Modifier.width(32.dp)) { - if (current) { - Icon( - imageVector = Icons.Default.RadioButtonChecked, - contentDescription = stringResource(R.string.account_switch_active_account), - tint = MaterialTheme.colors.secondary - ) - } - } } - } - IconButton( - onClick = { accountStateViewModel.logOff(acc.npub) } - ) { - Icon( - imageVector = Icons.Default.Logout, - contentDescription = stringResource(R.string.log_out), - tint = MaterialTheme.colors.onSurface - ) + IconButton( + onClick = { accountStateViewModel.logOff(acc.npub) } + ) { + Icon( + imageVector = Icons.Default.Logout, + contentDescription = stringResource(R.string.log_out), + tint = MaterialTheme.colors.onSurface + ) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 58c9140e9..0118af459 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -35,11 +35,11 @@ class AccountStateViewModel() : ViewModel() { private fun tryLoginExistingAccount() { LocalPreferences.loadFromEncryptedStorage()?.let { - login(it) + startUI(it) } } - fun login(key: String) { + fun startUI(key: String) { val pattern = Pattern.compile(".+@.+\\.[a-z]+") val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.toByteArray() @@ -56,7 +56,8 @@ class AccountStateViewModel() : ViewModel() { Account(Persona(Hex.decode(key))) } - login(account) + LocalPreferences.updatePrefsForLogin(account) + startUI(account) } fun switchUser(npub: String) { @@ -67,24 +68,22 @@ class AccountStateViewModel() : ViewModel() { fun newKey() { val account = Account(Persona()) - login(account) + // saves to local preferences + LocalPreferences.updatePrefsForLogin(account) + startUI(account) } @OptIn(DelicateCoroutinesApi::class) - fun login(account: Account) { - LocalPreferences.updatePrefsForLogin(account) - + fun startUI(account: Account) { if (account.loggedIn.privKey != null) { _accountContent.update { AccountState.LoggedIn(account) } } else { _accountContent.update { AccountState.LoggedInViewOnly(account) } } - val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { ServiceManager.start(account) } - GlobalScope.launch(Dispatchers.Main) { account.saveable.observeForever(saveListener) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 844df8e4d..ec45c3aff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -158,7 +158,7 @@ fun LoginPage( keyboardActions = KeyboardActions( onGo = { try { - accountViewModel.login(key.value.text) + accountViewModel.startUI(key.value.text) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } @@ -237,7 +237,7 @@ fun LoginPage( if (acceptedTerms.value && key.value.text.isNotBlank()) { try { - accountViewModel.login(key.value.text) + accountViewModel.startUI(key.value.text) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) }