Reducing the use of Slow disk access of Local Preferences.

This commit is contained in:
Vitor Pamplona
2023-04-05 08:44:48 -04:00
parent 7b539e63bd
commit 924b21cdfc
4 changed files with 143 additions and 129 deletions

View File

@@ -27,10 +27,7 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
data class AccountInfo( data class AccountInfo(
val npub: String, val npub: String,
val hasPrivKey: Boolean, val hasPrivKey: Boolean = false
val current: Boolean,
val displayName: String?,
val profilePicture: String?
) )
private object PrefKeys { private object PrefKeys {
@@ -38,8 +35,6 @@ private object PrefKeys {
const val SAVED_ACCOUNTS = "all_saved_accounts" const val SAVED_ACCOUNTS = "all_saved_accounts"
const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PRIVKEY = "nostr_privkey"
const val NOSTR_PUBKEY = "nostr_pubkey" 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 FOLLOWING_CHANNELS = "following_channels"
const val HIDDEN_USERS = "hidden_users" const val HIDDEN_USERS = "hidden_users"
const val RELAYS = "relays" const val RELAYS = "relays"
@@ -59,53 +54,73 @@ private val gson = GsonBuilder().create()
object LocalPreferences { object LocalPreferences {
private const val comma = "," private const val comma = ","
private var currentAccount: String? private var _currentAccount: String? = null
get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
set(npub) { private fun currentAccount(): String? {
val prefs = encryptedPreferences() if (_currentAccount == null) {
prefs.edit().apply { _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) putString(PrefKeys.CURRENT_ACCOUNT, npub)
}.apply() }.apply()
} }
}
private val savedAccounts: List<String> private var _savedAccounts: List<String>? = null
get() = encryptedPreferences()
.getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() private fun savedAccounts(): List<String> {
if (_savedAccounts == null) {
_savedAccounts = encryptedPreferences()
.getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf()
}
return _savedAccounts!!
}
private fun updateSavedAccounts(accounts: List<String>) {
if (_savedAccounts != accounts) {
_savedAccounts = accounts
encryptedPreferences().edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply()
}
}
private val prefsDirPath: String private val prefsDirPath: String
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
private fun addAccount(npub: String) { private fun addAccount(npub: String) {
val accounts = savedAccounts.toMutableList() val accounts = savedAccounts().toMutableList()
if (npub !in accounts) { if (npub !in accounts) {
accounts.add(npub) 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) { private fun setCurrentAccount(account: Account) {
val npub = account.userProfile().pubkeyNpub() val npub = account.userProfile().pubkeyNpub()
currentAccount = npub updateCurrentAccount(npub)
addAccount(npub) addAccount(npub)
} }
fun switchToAccount(npub: String) { fun switchToAccount(npub: String) {
currentAccount = npub updateCurrentAccount(npub)
} }
/** /**
* Removes the account from the app level shared preferences * Removes the account from the app level shared preferences
*/ */
private fun removeAccount(npub: String) { private fun removeAccount(npub: String) {
val accounts = savedAccounts.toMutableList() val accounts = savedAccounts().toMutableList()
if (accounts.remove(npub)) { if (accounts.remove(npub)) {
val prefs = encryptedPreferences() updateSavedAccounts(accounts)
prefs.edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply()
} }
} }
@@ -145,11 +160,11 @@ object LocalPreferences {
removeAccount(npub) removeAccount(npub)
deleteUserPreferenceFile(npub) deleteUserPreferenceFile(npub)
if (savedAccounts.isEmpty()) { if (savedAccounts().isEmpty()) {
val appPrefs = encryptedPreferences() val appPrefs = encryptedPreferences()
appPrefs.edit().clear().apply() appPrefs.edit().clear().apply()
} else if (currentAccount == npub) { } else if (currentAccount() == npub) {
currentAccount = savedAccounts.elementAt(0) updateCurrentAccount(savedAccounts().elementAt(0))
} }
} }
@@ -159,17 +174,8 @@ object LocalPreferences {
} }
fun allSavedAccounts(): List<AccountInfo> { fun allSavedAccounts(): List<AccountInfo> {
return savedAccounts.map { npub -> return savedAccounts().map { npub ->
val prefs = encryptedPreferences(npub) AccountInfo(npub = 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)
)
} }
} }
@@ -189,13 +195,11 @@ object LocalPreferences {
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName())
putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture())
}.apply() }.apply()
} }
fun loadFromEncryptedStorage(): Account? { fun loadFromEncryptedStorage(): Account? {
encryptedPreferences(currentAccount).apply { encryptedPreferences(currentAccount()).apply {
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
@@ -247,7 +251,7 @@ object LocalPreferences {
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
return Account( val a = Account(
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
followingChannels, followingChannels,
hiddenUsers, hiddenUsers,
@@ -261,23 +265,25 @@ object LocalPreferences {
hideBlockAlertDialog, hideBlockAlertDialog,
latestContactList latestContactList
) )
return a
} }
} }
fun saveLastRead(route: String, timestampInSecs: Long) { fun saveLastRead(route: String, timestampInSecs: Long) {
encryptedPreferences(currentAccount).edit().apply { encryptedPreferences(currentAccount()).edit().apply {
putLong(PrefKeys.LAST_READ(route), timestampInSecs) putLong(PrefKeys.LAST_READ(route), timestampInSecs)
}.apply() }.apply()
} }
fun loadLastRead(route: String): Long { fun loadLastRead(route: String): Long {
encryptedPreferences(currentAccount).run { encryptedPreferences(currentAccount()).run {
return getLong(PrefKeys.LAST_READ(route), 0) return getLong(PrefKeys.LAST_READ(route), 0)
} }
} }
fun migrateSingleUserPrefs() { fun migrateSingleUserPrefs() {
if (currentAccount != null) return if (currentAccount() != null) return
val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return
val npub = Hex.decode(pubkey).toNpub() val npub = Hex.decode(pubkey).toNpub()
@@ -314,6 +320,6 @@ object LocalPreferences {
encryptedPreferences().edit().clear().apply() encryptedPreferences().edit().clear().apply()
addAccount(npub) addAccount(npub)
currentAccount = npub updateCurrentAccount(npub)
} }
} }

View File

@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -24,10 +23,8 @@ import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack 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.Logout
import androidx.compose.material.icons.filled.RadioButtonChecked import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@@ -45,14 +42,15 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R 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.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
import nostr.postr.bechToBytes
import nostr.postr.toHex
@Composable @Composable
fun AccountSwitchBottomSheet( fun AccountSwitchBottomSheet(
@@ -82,90 +80,101 @@ fun AccountSwitchBottomSheet(
accounts.forEach { acc -> accounts.forEach { acc ->
val current = accountUser.pubkeyNpub() == acc.npub val current = accountUser.pubkeyNpub() == acc.npub
Row( val baseUser = try {
modifier = Modifier.fillMaxWidth(), LocalCache.getOrCreateUser(decodePublicKey(acc.npub).toHexKey())
verticalAlignment = Alignment.CenterVertically } catch (e: Exception) {
) { null
}
if (baseUser != null) {
val userState by baseUser.live().metadata.observeAsState()
val user = userState?.user ?: return
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.weight(1f)
.clickable {
accountStateViewModel.switchUser(acc.npub)
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(16.dp, 16.dp) .weight(1f)
.weight(1f), .clickable {
accountStateViewModel.switchUser(acc.npub)
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Box( Row(
modifier = Modifier modifier = Modifier
.width(55.dp) .padding(16.dp, 16.dp)
.padding(0.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( Box(
modifier = Modifier modifier = Modifier
.size(20.dp) .width(55.dp)
.align(Alignment.TopEnd) .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( Icon(
imageVector = Icons.Default.Key, imageVector = Icons.Default.RadioButtonChecked,
contentDescription = stringResource(R.string.account_switch_has_private_key), contentDescription = stringResource(R.string.account_switch_active_account),
modifier = Modifier.size(20.dp), tint = MaterialTheme.colors.secondary
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()
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( IconButton(
onClick = { accountStateViewModel.logOff(acc.npub) } onClick = { accountStateViewModel.logOff(acc.npub) }
) { ) {
Icon( Icon(
imageVector = Icons.Default.Logout, imageVector = Icons.Default.Logout,
contentDescription = stringResource(R.string.log_out), contentDescription = stringResource(R.string.log_out),
tint = MaterialTheme.colors.onSurface tint = MaterialTheme.colors.onSurface
) )
}
} }
} }
} }

View File

@@ -35,11 +35,11 @@ class AccountStateViewModel() : ViewModel() {
private fun tryLoginExistingAccount() { private fun tryLoginExistingAccount() {
LocalPreferences.loadFromEncryptedStorage()?.let { LocalPreferences.loadFromEncryptedStorage()?.let {
login(it) startUI(it)
} }
} }
fun login(key: String) { fun startUI(key: String) {
val pattern = Pattern.compile(".+@.+\\.[a-z]+") val pattern = Pattern.compile(".+@.+\\.[a-z]+")
val parsed = Nip19.uriToRoute(key) val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.toByteArray() val pubKeyParsed = parsed?.hex?.toByteArray()
@@ -56,7 +56,8 @@ class AccountStateViewModel() : ViewModel() {
Account(Persona(Hex.decode(key))) Account(Persona(Hex.decode(key)))
} }
login(account) LocalPreferences.updatePrefsForLogin(account)
startUI(account)
} }
fun switchUser(npub: String) { fun switchUser(npub: String) {
@@ -67,24 +68,22 @@ class AccountStateViewModel() : ViewModel() {
fun newKey() { fun newKey() {
val account = Account(Persona()) val account = Account(Persona())
login(account) // saves to local preferences
LocalPreferences.updatePrefsForLogin(account)
startUI(account)
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun login(account: Account) { fun startUI(account: Account) {
LocalPreferences.updatePrefsForLogin(account)
if (account.loggedIn.privKey != null) { if (account.loggedIn.privKey != null) {
_accountContent.update { AccountState.LoggedIn(account) } _accountContent.update { AccountState.LoggedIn(account) }
} else { } else {
_accountContent.update { AccountState.LoggedInViewOnly(account) } _accountContent.update { AccountState.LoggedInViewOnly(account) }
} }
val scope = CoroutineScope(Job() + Dispatchers.IO) val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch { scope.launch {
ServiceManager.start(account) ServiceManager.start(account)
} }
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
account.saveable.observeForever(saveListener) account.saveable.observeForever(saveListener)
} }

View File

@@ -158,7 +158,7 @@ fun LoginPage(
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onGo = { onGo = {
try { try {
accountViewModel.login(key.value.text) accountViewModel.startUI(key.value.text)
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = context.getString(R.string.invalid_key) errorMessage = context.getString(R.string.invalid_key)
} }
@@ -237,7 +237,7 @@ fun LoginPage(
if (acceptedTerms.value && key.value.text.isNotBlank()) { if (acceptedTerms.value && key.value.text.isNotBlank()) {
try { try {
accountViewModel.login(key.value.text) accountViewModel.startUI(key.value.text)
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = context.getString(R.string.invalid_key) errorMessage = context.getString(R.string.invalid_key)
} }