kopia lustrzana https://github.com/vitorpamplona/amethyst
Use multiple preference files for different accounts
rodzic
b7f8241a08
commit
3a2403b344
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="C:\Users\aiong\.android\avd\Pixel_6_API_33.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2023-03-11T16:38:31.686457400Z" />
|
||||
</component>
|
||||
</project>
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:name=".Amethyst"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/amethyst"
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class Amethyst : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Amethyst
|
||||
private set
|
||||
}
|
||||
}
|
|
@ -1,20 +1,22 @@
|
|||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
object EncryptedStorage {
|
||||
private const val PREFERENCES_NAME = "secret_keeper"
|
||||
|
||||
fun preferences(context: Context): EncryptedSharedPreferences {
|
||||
fun preferences(npub: String? = null): EncryptedSharedPreferences {
|
||||
val context = Amethyst.instance
|
||||
val masterKey: MasterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
val preferencesName = if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
||||
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
PREFERENCES_NAME,
|
||||
preferencesName,
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
|
@ -11,53 +12,77 @@ import com.vitorpamplona.amethyst.service.model.Event
|
|||
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
||||
import nostr.postr.Persona
|
||||
import nostr.postr.toHex
|
||||
import nostr.postr.toNpub
|
||||
import java.util.Locale
|
||||
|
||||
const val DEBUG_PLAINTEXT_PREFERENCES = true
|
||||
|
||||
data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?)
|
||||
|
||||
class LocalPreferences(context: Context) {
|
||||
private object PrefKeys {
|
||||
const val CURRENT_ACCOUNT = "currently_logged_in_account"
|
||||
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"
|
||||
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
|
||||
const val LANGUAGE_PREFS = "languagePreferences"
|
||||
const val TRANSLATE_TO = "translateTo"
|
||||
const val ZAP_AMOUNTS = "zapAmounts"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
private fun prefKeysForAccount(npub: String) = object {
|
||||
val NOSTR_PRIVKEY = "$npub/nostr_privkey"
|
||||
val NOSTR_PUBKEY = "$npub/nostr_pubkey"
|
||||
val DISPLAY_NAME = "$npub/display_name"
|
||||
val PROFILE_PICTURE_URL = "$npub/profile_picture"
|
||||
val FOLLOWING_CHANNELS = "$npub/following_channels"
|
||||
val HIDDEN_USERS = "$npub/hidden_users"
|
||||
val RELAYS = "$npub/relays"
|
||||
val DONT_TRANSLATE_FROM = "$npub/dontTranslateFrom"
|
||||
val LANGUAGE_PREFS = "$npub/languagePreferences"
|
||||
val TRANSLATE_TO = "$npub/translateTo"
|
||||
val ZAP_AMOUNTS = "$npub/zapAmounts"
|
||||
val LATEST_CONTACT_LIST = "$npub/latestContactList"
|
||||
val HIDE_DELETE_REQUEST_INFO = "$npub/hideDeleteRequestInfo"
|
||||
// val LAST_READ: (String) -> String = { route -> "$npub/last_read_route_$route" }
|
||||
private val gson = GsonBuilder().create()
|
||||
|
||||
object LocalPreferences {
|
||||
private var currentAccount: String?
|
||||
get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null)
|
||||
set(npub) {
|
||||
val prefs = encryptedPreferences()
|
||||
prefs.edit().apply {
|
||||
putString(PrefKeys.CURRENT_ACCOUNT, npub)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
private val savedAccounts: Set<String>
|
||||
get() = encryptedPreferences().getStringSet(PrefKeys.SAVED_ACCOUNTS, null) ?: setOf()
|
||||
|
||||
private fun addAccount(npub: String) {
|
||||
val accounts = savedAccounts.toMutableSet()
|
||||
accounts.add(npub)
|
||||
val prefs = encryptedPreferences()
|
||||
prefs.edit().apply {
|
||||
putStringSet(PrefKeys.SAVED_ACCOUNTS, accounts)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
private object PrefKeys {
|
||||
const val CURRENT_ACCOUNT = "currentlyLoggedInAccount"
|
||||
|
||||
// val NOSTR_PRIVKEY = "nostr_privkey"
|
||||
// val NOSTR_PUBKEY = "nostr_pubkey"
|
||||
// val FOLLOWING_CHANNELS = "following_channels"
|
||||
// val HIDDEN_USERS = "hidden_users"
|
||||
// val RELAYS = "relays"
|
||||
// val DONT_TRANSLATE_FROM = "dontTranslateFrom"
|
||||
// val LANGUAGE_PREFS = "languagePreferences"
|
||||
// val TRANSLATE_TO = "translateTo"
|
||||
// val ZAP_AMOUNTS = "zapAmounts"
|
||||
// val LATEST_CONTACT_LIST = "latestContactList"
|
||||
// val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
private fun removeAccount(npub: String) {
|
||||
val accounts = savedAccounts.toMutableSet()
|
||||
accounts.remove(npub)
|
||||
val prefs = encryptedPreferences()
|
||||
prefs.edit().apply {
|
||||
putStringSet(PrefKeys.SAVED_ACCOUNTS, accounts)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
private val encryptedPreferences = EncryptedStorage.preferences(context)
|
||||
private val gson = GsonBuilder().create()
|
||||
private fun encryptedPreferences(npub: String? = null): SharedPreferences {
|
||||
return if (DEBUG_PLAINTEXT_PREFERENCES) {
|
||||
val preferenceFile = if (npub == null) "testing_only" else "testing_only_$npub"
|
||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
} else {
|
||||
return EncryptedStorage.preferences(npub)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearEncryptedStorage() {
|
||||
encryptedPreferences.edit().apply {
|
||||
encryptedPreferences.all.keys.forEach {
|
||||
fun clearEncryptedStorage(npub: String? = null) {
|
||||
val encPrefs = encryptedPreferences(npub)
|
||||
encPrefs.edit().apply {
|
||||
encPrefs.all.keys.forEach {
|
||||
remove(it)
|
||||
}
|
||||
// encryptedPreferences.all.keys.filter {
|
||||
|
@ -69,76 +94,64 @@ class LocalPreferences(context: Context) {
|
|||
}
|
||||
|
||||
fun findAllLocalAccounts(): List<AccountInfo> {
|
||||
encryptedPreferences.apply {
|
||||
val currentAccount = getString(PrefKeys.CURRENT_ACCOUNT, null)
|
||||
return encryptedPreferences.all.keys.filter {
|
||||
it.endsWith("nostr_pubkey")
|
||||
}.map {
|
||||
val npub = it.substringBefore("/")
|
||||
val myPrefs = prefKeysForAccount(npub)
|
||||
AccountInfo(
|
||||
npub,
|
||||
npub == currentAccount,
|
||||
getString(myPrefs.DISPLAY_NAME, null),
|
||||
getString(myPrefs.PROFILE_PICTURE_URL, null)
|
||||
)
|
||||
}
|
||||
return savedAccounts.map { npub ->
|
||||
val prefs = encryptedPreferences(npub)
|
||||
|
||||
AccountInfo(
|
||||
npub = npub,
|
||||
current = npub == currentAccount,
|
||||
displayName = prefs.getString(PrefKeys.DISPLAY_NAME, null),
|
||||
profilePicture = prefs.getString(PrefKeys.PROFILE_PICTURE_URL, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToEncryptedStorage(account: Account) {
|
||||
val npub = account.loggedIn.pubKey.toNpub()
|
||||
val myPrefs = prefKeysForAccount(npub)
|
||||
|
||||
encryptedPreferences.edit().apply {
|
||||
putString(PrefKeys.CURRENT_ACCOUNT, npub)
|
||||
account.loggedIn.privKey?.let { putString(myPrefs.NOSTR_PRIVKEY, it.toHex()) }
|
||||
account.loggedIn.pubKey.let { putString(myPrefs.NOSTR_PUBKEY, it.toHex()) }
|
||||
putStringSet(myPrefs.FOLLOWING_CHANNELS, account.followingChannels)
|
||||
putStringSet(myPrefs.HIDDEN_USERS, account.hiddenUsers)
|
||||
putString(myPrefs.RELAYS, gson.toJson(account.localRelays))
|
||||
putStringSet(myPrefs.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
||||
putString(myPrefs.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
|
||||
putString(myPrefs.TRANSLATE_TO, account.translateTo)
|
||||
putString(myPrefs.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
||||
putString(myPrefs.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||
putBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo)
|
||||
}.apply()
|
||||
fun setCurrentAccount(account: Account) {
|
||||
val npub = account.userProfile().pubkeyNpub()
|
||||
currentAccount = npub
|
||||
addAccount(npub)
|
||||
}
|
||||
|
||||
fun saveCurrentAccountMetadata(account: Account) {
|
||||
val myPrefs = prefKeysForAccount(account.loggedIn.pubKey.toNpub())
|
||||
|
||||
encryptedPreferences.edit().apply {
|
||||
putString(myPrefs.DISPLAY_NAME, account.userProfile().toBestDisplayName())
|
||||
putString(myPrefs.PROFILE_PICTURE_URL, account.userProfile().profilePicture())
|
||||
fun saveToEncryptedStorage(account: Account) {
|
||||
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
|
||||
prefs.edit().apply {
|
||||
account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) }
|
||||
account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) }
|
||||
putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels)
|
||||
putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers)
|
||||
putString(PrefKeys.RELAYS, gson.toJson(account.localRelays))
|
||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
||||
putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
|
||||
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
|
||||
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo)
|
||||
putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName())
|
||||
putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture())
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun loadFromEncryptedStorage(): Account? {
|
||||
encryptedPreferences.apply {
|
||||
val npub = getString(PrefKeys.CURRENT_ACCOUNT, null) ?: return null
|
||||
val myPrefs = prefKeysForAccount(npub)
|
||||
|
||||
val pubKey = getString(myPrefs.NOSTR_PUBKEY, null) ?: return null
|
||||
val privKey = getString(myPrefs.NOSTR_PRIVKEY, null)
|
||||
val followingChannels = getStringSet(myPrefs.FOLLOWING_CHANNELS, null) ?: setOf()
|
||||
val hiddenUsers = getStringSet(myPrefs.HIDDEN_USERS, emptySet()) ?: setOf()
|
||||
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()
|
||||
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
|
||||
val localRelays = gson.fromJson(
|
||||
getString(myPrefs.RELAYS, "[]"),
|
||||
getString(PrefKeys.RELAYS, "[]"),
|
||||
object : TypeToken<Set<RelaySetupInfo>>() {}.type
|
||||
) ?: setOf<RelaySetupInfo>()
|
||||
|
||||
val dontTranslateFrom = getStringSet(myPrefs.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||
val translateTo = getString(myPrefs.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||
|
||||
val zapAmountChoices = gson.fromJson(
|
||||
getString(myPrefs.ZAP_AMOUNTS, "[]"),
|
||||
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
||||
object : TypeToken<List<Long>>() {}.type
|
||||
) ?: listOf(500L, 1000L, 5000L)
|
||||
|
||||
val latestContactList = try {
|
||||
getString(myPrefs.LATEST_CONTACT_LIST, null)?.let {
|
||||
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
|
||||
Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
@ -147,7 +160,7 @@ class LocalPreferences(context: Context) {
|
|||
}
|
||||
|
||||
val languagePreferences = try {
|
||||
getString(myPrefs.LANGUAGE_PREFS, null)?.let {
|
||||
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
|
||||
gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type) as Map<String, String>
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
|
@ -155,7 +168,7 @@ class LocalPreferences(context: Context) {
|
|||
mapOf()
|
||||
}
|
||||
|
||||
val hideDeleteRequestInfo = getBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, false)
|
||||
val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false)
|
||||
|
||||
return Account(
|
||||
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
|
||||
|
@ -173,13 +186,13 @@ class LocalPreferences(context: Context) {
|
|||
}
|
||||
|
||||
fun saveLastRead(route: String, timestampInSecs: Long) {
|
||||
encryptedPreferences.edit().apply {
|
||||
encryptedPreferences(currentAccount).edit().apply {
|
||||
putLong(PrefKeys.LAST_READ(route), timestampInSecs)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun loadLastRead(route: String): Long {
|
||||
encryptedPreferences.run {
|
||||
encryptedPreferences(currentAccount).run {
|
||||
return getLong(PrefKeys.LAST_READ(route), 0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ object NotificationCache {
|
|||
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
LocalPreferences(context).saveLastRead(route, timestampInSecs)
|
||||
LocalPreferences.saveLastRead(route, timestampInSecs)
|
||||
live.invalidateData()
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ object NotificationCache {
|
|||
fun load(route: String, context: Context): Long {
|
||||
var lastTime = lastReadByRoute[route]
|
||||
if (lastTime == null) {
|
||||
lastTime = LocalPreferences(context).loadLastRead(route)
|
||||
lastTime = LocalPreferences.loadLastRead(route)
|
||||
lastReadByRoute[route] = lastTime
|
||||
}
|
||||
return lastTime
|
||||
|
|
|
@ -14,7 +14,6 @@ import coil.ImageLoader
|
|||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
|
@ -54,7 +53,7 @@ class MainActivity : FragmentActivity() {
|
|||
// A surface container using the 'background' color from the theme
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel {
|
||||
AccountStateViewModel(LocalPreferences(applicationContext))
|
||||
AccountStateViewModel()
|
||||
}
|
||||
|
||||
AccountScreen(accountStateViewModel, startingPage)
|
||||
|
|
|
@ -1,54 +1,81 @@
|
|||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun AccountSwitchBottomSheet(
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
sheetState: ModalBottomSheetState
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val localPrefs = LocalPreferences(context)
|
||||
val accounts = localPrefs.findAllLocalAccounts()
|
||||
val accounts = LocalPreferences.findAllLocalAccounts()
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
@ -56,9 +83,7 @@ fun AccountSwitchBottomSheet(
|
|||
val accountUserState by account.userProfile().live().metadata.observeAsState()
|
||||
val accountUser = accountUserState?.user ?: return
|
||||
|
||||
LaunchedEffect(key1 = accountUser) {
|
||||
localPrefs.saveCurrentAccountMetadata(account)
|
||||
}
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
Row(
|
||||
|
@ -114,9 +139,98 @@ fun AccountSwitchBottomSheet(
|
|||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = { coroutineScope.launch { sheetState.hide() } }) {
|
||||
TextButton(onClick = { popupExpanded = true }) {
|
||||
Text("Add New Account")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (popupExpanded) {
|
||||
Dialog(
|
||||
onDismissRequest = { popupExpanded = false },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colors.surface),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val key = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
var showPassword by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val autofillNode = AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) }
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = key.value,
|
||||
onValueChange = { key.value = it },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.nsec_npub_hex_private_key),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
Icon(
|
||||
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription = if (showPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = {
|
||||
try {
|
||||
accountStateViewModel.login(key.value.text)
|
||||
} catch (e: Exception) {
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colors.error,
|
||||
style = MaterialTheme.typography.caption
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import nostr.postr.Persona
|
|||
import nostr.postr.bechToBytes
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AccountStateViewModel(private val localPreferences: LocalPreferences) : ViewModel() {
|
||||
class AccountStateViewModel() : ViewModel() {
|
||||
private val _accountContent = MutableStateFlow<AccountState>(AccountState.LoggedOff)
|
||||
val accountContent = _accountContent.asStateFlow()
|
||||
|
||||
|
@ -26,7 +26,7 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi
|
|||
|
||||
// Keeps it in the the UI thread to void blinking the login page.
|
||||
// viewModelScope.launch(Dispatchers.IO) {
|
||||
localPreferences.loadFromEncryptedStorage()?.let {
|
||||
LocalPreferences.loadFromEncryptedStorage()?.let {
|
||||
login(it)
|
||||
}
|
||||
// }
|
||||
|
@ -47,18 +47,19 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi
|
|||
Account(Persona(Hex.decode(key)))
|
||||
}
|
||||
|
||||
localPreferences.saveToEncryptedStorage(account)
|
||||
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
login(account)
|
||||
}
|
||||
|
||||
fun newKey() {
|
||||
val account = Account(Persona())
|
||||
localPreferences.saveToEncryptedStorage(account)
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
login(account)
|
||||
}
|
||||
|
||||
fun login(account: Account) {
|
||||
LocalPreferences.setCurrentAccount(account)
|
||||
|
||||
if (account.loggedIn.privKey != null) {
|
||||
_accountContent.update { AccountState.LoggedIn(account) }
|
||||
} else {
|
||||
|
@ -77,7 +78,7 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi
|
|||
|
||||
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
localPreferences.saveToEncryptedStorage(it.account)
|
||||
LocalPreferences.saveToEncryptedStorage(it.account)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,6 +101,6 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi
|
|||
|
||||
_accountContent.update { AccountState.LoggedOff }
|
||||
|
||||
localPreferences.clearEncryptedStorage()
|
||||
LocalPreferences.clearEncryptedStorage()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||
ModalBottomSheetLayout(
|
||||
sheetState = sheetState,
|
||||
sheetContent = {
|
||||
AccountSwitchBottomSheet(accountViewModel = accountViewModel, sheetState = sheetState)
|
||||
AccountSwitchBottomSheet(accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel, sheetState = sheetState)
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
Ładowanie…
Reference in New Issue