From a0238da2fda30e0e3d1a12831d3e38d15a9bdb07 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 20 Nov 2023 16:37:26 -0300 Subject: [PATCH] new ui, use NostrSignerInternal from quartz --- .../nostrsigner/LocalPreferences.kt | 25 +- .../greenart7c3/nostrsigner/MainActivity.kt | 8 +- .../greenart7c3/nostrsigner/SignerProvider.kt | 29 +- .../greenart7c3/nostrsigner/models/Account.kt | 8 +- .../nostrsigner/models/IntentData.kt | 36 ++- .../nostrsigner/service/AmberUtils.kt | 62 +---- .../nostrsigner/service/IntentUtils.kt | 12 +- .../nostrsigner/service/KeyPair.kt | 27 -- .../nostrsigner/service/model/AmberEvent.kt | 19 +- .../nostrsigner/ui/AccountScreen.kt | 5 +- .../nostrsigner/ui/AccountStateViewModel.kt | 8 +- .../greenart7c3/nostrsigner/ui/HomeScreen.kt | 50 +--- .../greenart7c3/nostrsigner/ui/LoginScreen.kt | 35 ++- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 88 +++--- .../ui/actions/AccountBackupDialog.kt | 2 +- .../ui/components/AcceptRejectButtons.kt | 20 +- .../ui/components/EncryptDecryptData.kt | 69 ++++- .../nostrsigner/ui/components/EventData.kt | 81 ++---- .../ui/components/LoginWithPubKey.kt | 258 +++++++++++++++--- .../nostrsigner/ui/components/PostButton.kt | 2 +- .../nostrsigner/ui/components/RawJson.kt | 29 +- .../ui/components/RawJsonButton.kt | 11 +- .../ui/previews/EventDataPreview.kt | 1 + .../greenart7c3/nostrsigner/ui/theme/Theme.kt | 28 +- app/src/main/res/drawable/manage_accounts.xml | 7 - app/src/main/res/drawable/relays.xml | 4 - 26 files changed, 519 insertions(+), 405 deletions(-) delete mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/service/KeyPair.kt delete mode 100644 app/src/main/res/drawable/manage_accounts.xml delete mode 100644 app/src/main/res/drawable/relays.xml diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index fb49029d..0d5140ba 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -4,11 +4,8 @@ import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import androidx.compose.runtime.Immutable -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.greenart7c3.nostrsigner.models.Account -import com.greenart7c3.nostrsigner.models.History -import com.greenart7c3.nostrsigner.service.KeyPair +import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toNpub @@ -28,7 +25,6 @@ private object PrefKeys { const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PUBKEY = "nostr_pubkey" const val REMEMBER_APPS = "remember_apps" - const val HISTORY = "history" const val ACCOUNT_NAME = "account_name" } @@ -184,13 +180,11 @@ object LocalPreferences { fun saveToEncryptedStorage(account: Account) { val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) prefs.edit().apply { - account.keyPair.privKey.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) } + account.keyPair.privKey.let { putString(PrefKeys.NOSTR_PRIVKEY, it?.toHexKey()) } account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) } val jsonObject = JSONObject(account.savedApps.toMap()) putString(PrefKeys.REMEMBER_APPS, jsonObject.toString()) putString(PrefKeys.ACCOUNT_NAME, account.name) - val historyObject = Gson().toJson(account.history) - putString(PrefKeys.HISTORY, historyObject) }.apply() } @@ -210,14 +204,6 @@ object LocalPreferences { } } - fun loadHistory(npub: String?): MutableList { - encryptedPreferences(npub).apply { - val historyJson = getString(PrefKeys.HISTORY, "[]") ?: "[]" - val listType = object : TypeToken>() {}.type - return Gson().fromJson>(historyJson, listType).toMutableList() - } - } - fun loadFromEncryptedStorage(npub: String?): Account? { encryptedPreferences(npub).apply { val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null @@ -236,15 +222,10 @@ object LocalPreferences { } val name = getString(PrefKeys.ACCOUNT_NAME, "") ?: "" - val historyJson = getString(PrefKeys.HISTORY, "[]") ?: "[]" - val listType = object : TypeToken>() {}.type - val history = Gson().fromJson>(historyJson, listType).toMutableList() - return Account( keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), name = name, - savedApps = outputMap, - history = history + savedApps = outputMap ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt index 16fae46f..64f44f6a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt @@ -22,6 +22,12 @@ class MainActivity : AppCompatActivity() { setContent { val packageName = callingPackage + val appName = if (packageName != null) { + val info = applicationContext.packageManager.getApplicationInfo(packageName, 0) + applicationContext.packageManager.getApplicationLabel(info).toString() + } else { + null + } NostrSignerTheme { Surface( @@ -31,7 +37,7 @@ class MainActivity : AppCompatActivity() { val accountStateViewModel: AccountStateViewModel = viewModel { AccountStateViewModel(intent.getStringExtra("current_user")) } - AccountScreen(accountStateViewModel, intent, packageName) + AccountScreen(accountStateViewModel, intent, packageName, appName) } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index ef85a7bd..bd04de60 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -5,14 +5,10 @@ import android.content.ContentValues import android.database.Cursor import android.database.MatrixCursor import android.net.Uri -import com.greenart7c3.nostrsigner.models.History import com.greenart7c3.nostrsigner.models.SignerType -import com.greenart7c3.nostrsigner.models.TimeUtils import com.greenart7c3.nostrsigner.service.AmberUtils -import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.events.Event -import com.vitorpamplona.quartz.events.LnZapRequestEvent class SignerProvider : ContentProvider() { override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { @@ -51,24 +47,19 @@ class SignerProvider : ContentProvider() { val isRemembered = account.savedApps[key] ?: false if (!isRemembered) return null - if (event is LnZapRequestEvent && event.tags.any { tag -> tag.any { t -> t == "anon" } }) { - val signedEvent = AmberUtils.getZapRequestEvent(event, account.keyPair.privKey) + var cursor: MatrixCursor? = null - val cursor = MatrixCursor(arrayOf("signature", "event")) - cursor.addRow(arrayOf(signedEvent.sig, signedEvent.toJson())) - return cursor + account.signer.sign(event.createdAt, event.kind, event.tags, event.content) { signedEvent -> + val localCursor = MatrixCursor(arrayOf("signature", "event")).also { + it.addRow(arrayOf(signedEvent.sig, signedEvent.toJson())) + } + cursor = localCursor } - val signedEvent = AmberUtils.getSignedEvent( - AmberEvent.fromJson(json), - account.keyPair.privKey - ) + while (cursor == null) { + // do nothing + } - account.history.add(History(packageName, "SIGN_EVENT", TimeUtils.now(), signedEvent.kind)) - LocalPreferences.saveToEncryptedStorage(account) - - val cursor = MatrixCursor(arrayOf("signature", "event")) - cursor.addRow(arrayOf(signedEvent.sig, signedEvent.toJson())) return cursor } "content://$appId.NIP04_DECRYPT", @@ -106,7 +97,6 @@ class SignerProvider : ContentProvider() { "Could not decrypt the message" } - account.history.add(History(packageName, stringType, TimeUtils.now(), null)) LocalPreferences.saveToEncryptedStorage(account) if (type == SignerType.NIP04_ENCRYPT && result == "Could not decrypt the message") { @@ -125,7 +115,6 @@ class SignerProvider : ContentProvider() { val isRemembered = account.savedApps[key] ?: false if (!isRemembered) return null - account.history.add(History(packageName, "GET_PUBLIC_KEY", TimeUtils.now(), null)) LocalPreferences.saveToEncryptedStorage(account) val cursor = MatrixCursor(arrayOf("signature")) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt index 254eacfe..c7aecd21 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt @@ -3,7 +3,9 @@ package com.greenart7c3.nostrsigner.models import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData -import com.greenart7c3.nostrsigner.service.KeyPair +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.signers.NostrSignerInternal class History( val appName: String, @@ -15,9 +17,9 @@ class History( @Stable class Account( val keyPair: KeyPair, + val signer: NostrSigner = NostrSignerInternal(keyPair), var name: String, - var savedApps: MutableMap, - var history: MutableList = mutableListOf() + var savedApps: MutableMap ) { val saveable: AccountLiveData = AccountLiveData(this) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt index 8499b54f..2321ea38 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt @@ -2,6 +2,39 @@ package com.greenart7c3.nostrsigner.models import com.vitorpamplona.quartz.encoders.HexKey +data class Permission( + val type: String, + val kind: Int?, + var checked: Boolean = true +) { + override fun toString(): String { + return when (type) { + "nip04_encrypt" -> { + "Encrypt data using nip 4" + } + "nip04_decrypt" -> { + "Decrypt data using nip 4" + } + "nip44_decrypt" -> { + "Decrypt data using nip 44" + } + "nip44_encrypt" -> { + "Encrypt data using nip 44" + } + "decrypt_zap_event" -> { + "Decrypt private zaps" + } + "sign_event" -> { + when (kind) { + 22242 -> "Client authentication" + else -> "Event kind $kind" + } + } + else -> type + } + } +} + class IntentData( val data: String, val name: String, @@ -10,7 +43,8 @@ class IntentData( val id: String, val callBackUrl: String?, val compression: CompressionType, - val returnType: ReturnType + val returnType: ReturnType, + val permissions: List? ) enum class SignerType { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt index 6e146c9f..96937b2b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt @@ -3,7 +3,6 @@ package com.greenart7c3.nostrsigner.service import android.util.Log import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.SignerType -import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.Nip44Version import com.vitorpamplona.quartz.crypto.decodeNIP44 @@ -13,7 +12,6 @@ import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.Event -import com.vitorpamplona.quartz.events.LnZapPrivateEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import fr.acinq.secp256k1.Hex import java.nio.charset.Charset @@ -23,49 +21,6 @@ import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec object AmberUtils { - fun getSignedEvent(unsignedEvent: AmberEvent, privateKey: ByteArray): AmberEvent { - val id = unsignedEvent.id.hexToByteArray() - val sig = CryptoUtils.sign(id, privateKey).toHexKey() - return AmberEvent( - unsignedEvent.id, - unsignedEvent.pubKey, - unsignedEvent.createdAt, - unsignedEvent.kind, - unsignedEvent.tags, - unsignedEvent.content, - sig - ) - } - fun getZapRequestEvent(localEvent: LnZapRequestEvent, privateKey: ByteArray): LnZapRequestEvent { - val originalNoteId = localEvent.zappedPost().firstOrNull() - val pubkey = localEvent.zappedAuthor()[0] - var privkey = privateKey - val encryptionPrivateKey = LnZapRequestEvent.createEncryptionPrivateKey( - privkey.toHexKey(), - originalNoteId ?: pubkey, - localEvent.createdAt - ) - val noteJson = (LnZapPrivateEvent.create(privkey, listOf(localEvent.tags[0], localEvent.tags[1]), localEvent.content)).toJson() - val encryptedContent = encryptPrivateZapMessage( - noteJson, - encryptionPrivateKey, - pubkey.hexToByteArray() - ) - var tags = localEvent.tags.filter { !it.contains("anon") } - tags = tags + listOf(listOf("anon", encryptedContent)) - privkey = encryptionPrivateKey // sign event with generated privkey - val pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey - - val id = AmberEvent.generateId( - pubKey, - localEvent.createdAt, - LnZapRequestEvent.kind, - tags, - "" - ) - val sig = CryptoUtils.sign(id, privkey) - return LnZapRequestEvent(id.toHexKey(), pubKey, localEvent.createdAt, tags, "", sig.toHexKey()) - } fun encryptOrDecryptData(data: String, type: SignerType, account: Account, pubKey: HexKey): String? { return when (type) { SignerType.DECRYPT_ZAP_EVENT -> { @@ -74,20 +29,20 @@ object AmberUtils { SignerType.NIP04_DECRYPT -> { CryptoUtils.decryptNIP04( data, - account.keyPair.privKey, + account.keyPair.privKey!!, Hex.decode(pubKey) ) } SignerType.NIP04_ENCRYPT -> { CryptoUtils.encryptNIP04( data, - account.keyPair.privKey, + account.keyPair.privKey!!, Hex.decode(pubKey) ) } SignerType.NIP44_ENCRYPT -> { val sharedSecret = CryptoUtils.getSharedSecretNIP44( - account.keyPair.privKey, + account.keyPair.privKey!!, pubKey.hexToByteArray() ) @@ -103,12 +58,12 @@ object AmberUtils { when (toDecrypt.v) { Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04( toDecrypt, - account.keyPair.privKey, + account.keyPair.privKey!!, pubKey.hexToByteArray() ) Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44( toDecrypt, - account.keyPair.privKey, + account.keyPair.privKey!!, pubKey.hexToByteArray() ) else -> null @@ -128,18 +83,18 @@ object AmberUtils { // if the receiver is logged in, these are the params. val pubkeyToUse = event.pubKey - event.getPrivateZapEvent(loggedInPrivateKey, pubkeyToUse)?.toJson() ?: "" + event.getPrivateZapEvent(loggedInPrivateKey!!, pubkeyToUse)?.toJson() ?: "" } else { // if the sender is logged in, these are the params val altPrivateKeyToUse = if (recipientPost != null) { LnZapRequestEvent.createEncryptionPrivateKey( - loggedInPrivateKey.toHexKey(), + loggedInPrivateKey!!.toHexKey(), recipientPost, event.createdAt ) } else if (recipientPK != null) { LnZapRequestEvent.createEncryptionPrivateKey( - loggedInPrivateKey.toHexKey(), + loggedInPrivateKey!!.toHexKey(), recipientPK, event.createdAt ) @@ -153,7 +108,6 @@ object AmberUtils { if (altPubKeyFromPrivate == event.pubKey) { val result = event.getPrivateZapEvent(altPrivateKeyToUse, recipientPK) - result?.toJson() ?: "" } else { null diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index f0bbea05..6df2fb83 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -2,11 +2,15 @@ package com.greenart7c3.nostrsigner.service import android.content.Intent import android.net.Uri +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import com.greenart7c3.nostrsigner.models.CompressionType import com.greenart7c3.nostrsigner.models.IntentData +import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.ReturnType import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.service.model.AmberEvent +import com.vitorpamplona.quartz.crypto.KeyPair import java.net.URLDecoder object IntentUtils { @@ -41,6 +45,11 @@ object IntentUtils { val compressionType = if (intent.extras?.getString("compression") == "gzip") CompressionType.GZIP else CompressionType.NONE val returnType = if (intent.extras?.getString("returnType") == "event") ReturnType.EVENT else ReturnType.SIGNATURE + val listType = object : TypeToken>() {}.type + val permissions = Gson().fromJson?>(intent.extras?.getString("permissions"), listType) + permissions?.forEach { + it.checked = true + } return IntentData( data, @@ -50,7 +59,8 @@ object IntentUtils { id, intent.extras?.getString("callbackUrl"), compressionType, - returnType + returnType, + permissions ) } return null diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/KeyPair.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/KeyPair.kt deleted file mode 100644 index 8048fcd5..00000000 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/KeyPair.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.greenart7c3.nostrsigner.service - -import com.vitorpamplona.quartz.crypto.CryptoUtils -import com.vitorpamplona.quartz.encoders.toHexKey - -class KeyPair( - privKey: ByteArray? = null, - pubKey: ByteArray? = null -) { - val privKey: ByteArray - val pubKey: ByteArray - - init { - if (privKey == null) { - this.privKey = CryptoUtils.privkeyCreate() - this.pubKey = CryptoUtils.pubkeyCreate(this.privKey) - } else { - // as private key is provided, ignore the public key and set keys according to private key - this.privKey = privKey - this.pubKey = pubKey ?: CryptoUtils.pubkeyCreate(privKey) - } - } - - override fun toString(): String { - return "KeyPair(privateKey=${privKey.toHexKey()}, publicKey=${pubKey.toHexKey()}" - } -} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt index 04027406..0e4cb14e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt @@ -15,12 +15,13 @@ import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import com.google.gson.annotations.SerializedName import com.greenart7c3.nostrsigner.models.TimeUtils -import com.greenart7c3.nostrsigner.service.KeyPair import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.EmojiUrl +import com.vitorpamplona.quartz.events.Event import fr.acinq.secp256k1.Hex import java.lang.reflect.Type import java.math.BigDecimal @@ -64,6 +65,8 @@ open class AmberEvent( override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) } + fun relay() = tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first() + override fun isSensitive() = tags.any { (it.size > 0 && it[0].equals("content-warning", true)) || (it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) || @@ -419,6 +422,18 @@ open class AmberEvent( else -> this } + fun toEvent(amberEvent: AmberEvent): Event { + return Event( + amberEvent.id, + amberEvent.pubKey, + amberEvent.createdAt, + amberEvent.kind, + amberEvent.tags, + amberEvent.content, + amberEvent.sig + ) + } + fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List>, content: String): ByteArray { val rawEvent = listOf( 0, @@ -444,7 +459,7 @@ open class AmberEvent( if (event.pubKey.isEmpty()) { val pubKey = keyPair.pubKey.toHexKey() val id = generateId(pubKey, event.createdAt, event.kind, event.tags, event.content) - val signature = CryptoUtils.sign(id, keyPair.privKey).toHexKey() + val signature = CryptoUtils.sign(id, keyPair.privKey!!).toHexKey() return AmberEvent(id.toHexKey(), pubKey, event.createdAt, event.kind, event.tags, event.content, signature) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt index c5cdaa0d..f9b090dc 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt @@ -13,7 +13,8 @@ import com.greenart7c3.nostrsigner.service.IntentUtils fun AccountScreen( accountStateViewModel: AccountStateViewModel, intent: Intent, - packageName: String? + packageName: String?, + appName: String? ) { val accountState by accountStateViewModel.accountContent.collectAsState() @@ -30,7 +31,7 @@ fun AccountScreen( is AccountState.LoggedIn -> { val intentData = IntentUtils.getIntentData(intent) - MainScreen(state.account, accountStateViewModel, intentData, packageName, state.route) + MainScreen(state.account, accountStateViewModel, intentData, packageName, appName, state.route) } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index f8c4edd0..73eb7f35 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.models.Account -import com.greenart7c3.nostrsigner.service.KeyPair +import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.bechToBytes import fr.acinq.secp256k1.Hex import kotlinx.coroutines.DelicateCoroutinesApi @@ -62,9 +62,9 @@ class AccountStateViewModel(npub: String?) : ViewModel() { fun startUI(key: String, route: String?) { val account = if (key.startsWith("nsec")) { - Account(KeyPair(privKey = key.bechToBytes()), "", mutableMapOf()) + Account(KeyPair(privKey = key.bechToBytes()), name = "", savedApps = mutableMapOf()) } else { - Account(KeyPair(Hex.decode(key)), "", mutableMapOf()) + Account(KeyPair(Hex.decode(key)), name = "", savedApps = mutableMapOf()) } LocalPreferences.updatePrefsForLogin(account) @@ -72,7 +72,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { } fun newKey() { - val account = Account(KeyPair(), "", mutableMapOf()) + val account = Account(KeyPair(), name = "", savedApps = mutableMapOf()) LocalPreferences.updatePrefsForLogin(account) startUI(account, null) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt index fedbbdd4..9a02d642 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt @@ -37,6 +37,7 @@ fun HomeScreen( modifier: Modifier, json: IntentData?, packageName: String?, + applicationName: String?, account: Account ) { val coroutineScope = rememberCoroutineScope() @@ -63,13 +64,11 @@ fun HomeScreen( val remember = remember { mutableStateOf(account.savedApps[key] ?: false) } - val shouldRunOnAccept = account.savedApps[key] ?: false LoginWithPubKey( - shouldRunOnAccept, - remember, - packageName, appName, - { + applicationName, + json.permissions, + { permissions -> val sig = account.keyPair.pubKey.toNpub() coroutineScope.launch { sendResult( @@ -81,15 +80,15 @@ fun HomeScreen( clipboardManager, sig, sig, - it + it, + permissions = permissions ) } return@LoginWithPubKey }, { context.getAppCompatActivity()?.finish() - }, - account + } ) } @@ -99,9 +98,11 @@ fun HomeScreen( } val shouldRunOnAccept = account.savedApps[key] ?: false EncryptDecryptData( + it.data, shouldRunOnAccept, remember, packageName, + applicationName, appName, it.type, { @@ -210,8 +211,9 @@ fun HomeScreen( remember, packageName, appName, + applicationName, event, - event.toJson(), + it.data, { if (event.pubKey != account.keyPair.pubKey.toHexKey()) { coroutineScope.launch { @@ -229,31 +231,8 @@ fun HomeScreen( } catch (e: Exception) { Event.fromJson(event.toJson()) } - if (localEvent is LnZapRequestEvent && localEvent.tags.any { tag -> tag.any { t -> t == "anon" } }) { - val resultEvent = AmberUtils.getZapRequestEvent( - localEvent, - account.keyPair.privKey - ) - coroutineScope.launch { - sendResult( - context, - packageName, - account, - key, - remember.value, - clipboardManager, - resultEvent.toJson(), - resultEvent.toJson(), - it, - resultEvent.kind - ) - } - } else { - val signedEvent = AmberUtils.getSignedEvent( - event, - account.keyPair.privKey - ) + account.signer.sign(localEvent.createdAt, localEvent.kind, localEvent.tags, localEvent.content) { signedEvent -> coroutineScope.launch { sendResult( context, @@ -263,9 +242,8 @@ fun HomeScreen( remember.value, clipboardManager, signedEvent.toJson(), - signedEvent.sig, - it, - signedEvent.kind + if (localEvent is LnZapRequestEvent && localEvent.tags.any { tag -> tag.any { t -> t == "anon" } }) signedEvent.toJson() else signedEvent.sig, + it ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt index 396a60fa..256d906f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt @@ -47,13 +47,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle +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.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.ui.theme.Font14SP @@ -76,7 +76,7 @@ fun LoginPage( .fillMaxSize() .navigationBarsPadding() .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.Center ) { // The first child is glued to the top. // Hence we have nothing at the top, an empty box is used. @@ -205,23 +205,20 @@ fun LoginPage( Text(text = stringResource(R.string.login)) } } - } - - // The last child is glued to the bottom. - ClickableText( - text = AnnotatedString(stringResource(R.string.generate_a_new_key)), - modifier = Modifier - .padding(20.dp) - .fillMaxWidth(), - onClick = { - accountViewModel.newKey() - }, - style = TextStyle( - fontSize = Font14SP, - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center + ClickableText( + text = AnnotatedString(stringResource(R.string.generate_a_new_key)), + modifier = Modifier + .padding(30.dp) + .fillMaxWidth(), + onClick = { + accountViewModel.newKey() + }, + style = TextStyle( + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) ) - ) + } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index e4cab0f3..75fc1ccc 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -9,20 +9,19 @@ import android.widget.Toast import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material3.Card +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton @@ -46,7 +45,7 @@ import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.unit.dp import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -59,14 +58,15 @@ import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.CompressionType -import com.greenart7c3.nostrsigner.models.History import com.greenart7c3.nostrsigner.models.IntentData +import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.ReturnType -import com.greenart7c3.nostrsigner.models.TimeUtils +import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.service.getAppCompatActivity import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet import com.greenart7c3.nostrsigner.ui.navigation.Route +import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder import com.vitorpamplona.quartz.encoders.toNpub import kotlinx.coroutines.launch import java.io.ByteArrayOutputStream @@ -83,21 +83,28 @@ fun sendResult( event: String, value: String, intentData: IntentData, - kind: Int? = null + permissions: List? = null ) { val activity = context.getAppCompatActivity() if (packageName != null) { + permissions?.filter { it.checked }?.forEach { + val type = it.type.toUpperCase(Locale.current) + val permissionKey = if (it.type == "sign_event") "$packageName-$type-${it.kind}" else "$packageName-$type" + account.savedApps[permissionKey] = true + } + if (rememberChoice) { account.savedApps[key] = true } - account.history.add(History(packageName, intentData.type.toString(), TimeUtils.now(), kind)) LocalPreferences.saveToEncryptedStorage(account) val intent = Intent() intent.putExtra("signature", value) intent.putExtra("id", intentData.id) intent.putExtra("event", event) - intent.putExtra("package", BuildConfig.APPLICATION_ID) + if (intentData.type == SignerType.GET_PUBLIC_KEY) { + intent.putExtra("package", BuildConfig.APPLICATION_ID) + } activity?.setResult(RESULT_OK, intent) } else if (intentData.callBackUrl != null) { if (intentData.returnType == ReturnType.SIGNATURE) { @@ -167,11 +174,12 @@ fun MainScreen( accountStateViewModel: AccountStateViewModel, json: IntentData?, packageName: String?, + appName: String?, route: String? ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() - val items = listOf(Route.Home, Route.Permissions, Route.History, Route.Settings) + val items = listOf(Route.Home, Route.Permissions, Route.Settings) var shouldShowBottomSheet by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState( @@ -180,6 +188,7 @@ fun MainScreen( ) val scope = rememberCoroutineScope() val listState = rememberLazyListState() + val interactionSource = remember { MutableInteractionSource() } Scaffold( floatingActionButton = { @@ -210,12 +219,21 @@ fun MainScreen( title = { val name = LocalPreferences.getAccountName(account.keyPair.pubKey.toNpub()) Row( - Modifier.clickable { - scope.launch { - sheetState.show() - shouldShowBottomSheet = true - } - }, + Modifier + .border( + border = ButtonDefaults.outlinedButtonBorder, + shape = ButtonBorder + ) + .padding(8.dp) + .clickable( + interactionSource = interactionSource, + indication = null + ) { + scope.launch { + sheetState.show() + shouldShowBottomSheet = true + } + }, verticalAlignment = Alignment.CenterVertically ) { Text(name.ifBlank { account.keyPair.pubKey.toNpub().toShortenHex() }) @@ -272,6 +290,7 @@ fun MainScreen( .padding(padding), json, packageName, + appName, account ) } @@ -304,41 +323,6 @@ fun MainScreen( } ) - composable( - Route.History.route, - content = { - val history = LocalPreferences.loadHistory(account.keyPair.pubKey.toNpub()).sortedWith(compareByDescending { it.time }) - LazyColumn( - Modifier - .fillMaxSize() - .padding(padding) - .padding(16.dp), - state = listState - ) { - items(history.size) { - Card( - Modifier - .fillMaxWidth() - .padding(4.dp) - ) { - Column( - Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - Text(history[it].appName) - Text(history[it].type.toLowerCase(Locale.current)) - history[it].kind?.let { - Text("$it") - } - Text(timeAgoShort(history[it].time, "now")) - } - } - } - } - } - ) - composable( Route.Permission.route, arguments = listOf(navArgument("packageName") { type = NavType.StringType }), diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt index 45548c69..ef312ac6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt @@ -178,7 +178,7 @@ private fun copyNSec( account: Account, clipboardManager: ClipboardManager ) { - account.keyPair.privKey.let { + account.keyPair.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) scope.launch { Toast.makeText( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt index 1c6ccc16..f3fe1ff5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.greenart7c3.nostrsigner.R @@ -28,16 +30,24 @@ fun AcceptRejectButtons( ) { Button( shape = ButtonBorder, - onClick = onReject + onClick = onReject, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF5A5554) + ) ) { Text(stringResource(R.string.reject)) } Spacer(modifier = Modifier.width(8.dp)) - Button( - shape = ButtonBorder, - onClick = onAccept + Row( + Modifier.weight(0.8f) ) { - Text(stringResource(R.string.accept)) + Button( + modifier = Modifier.fillMaxWidth(), + shape = ButtonBorder, + onClick = onAccept + ) { + Text(stringResource(R.string.accept)) + } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt index c372672f..601bc8cf 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt @@ -1,5 +1,6 @@ package com.greenart7c3.nostrsigner.ui.components +import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -12,19 +13,31 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.SignerType +import kotlinx.coroutines.launch @Composable fun EncryptDecryptData( + content: String, shouldRunOnAccept: Boolean, remember: MutableState, packageName: String?, + applicationName: String?, appName: String, type: SignerType, onAccept: () -> Unit, @@ -34,31 +47,51 @@ fun EncryptDecryptData( var showMore by androidx.compose.runtime.remember { mutableStateOf(false) } + val clipboardManager = LocalClipboardManager.current + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current Column( Modifier .fillMaxSize() .padding(8.dp) ) { - AppTitle(appName) + val message = when (type) { + SignerType.NIP44_ENCRYPT -> stringResource(R.string.encrypt_nip44) + SignerType.NIP04_ENCRYPT -> stringResource(R.string.encrypt_nip04) + SignerType.NIP44_DECRYPT -> stringResource(R.string.decrypt_nip44) + SignerType.NIP04_DECRYPT -> stringResource(R.string.decrypt_nip04) + SignerType.DECRYPT_ZAP_EVENT -> stringResource(R.string.decrypt_zap_event) + else -> stringResource(R.string.encrypt_decrypt) + } + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(applicationName ?: appName) + } + append(" requests $message") + }, + fontSize = 18.sp + ) Spacer(Modifier.size(4.dp)) Card( modifier = Modifier .fillMaxWidth() ) { Column(Modifier.padding(6.dp)) { - val message = when (type) { - SignerType.NIP44_ENCRYPT -> stringResource(R.string.encrypt_nip44) - SignerType.NIP04_ENCRYPT -> stringResource(R.string.encrypt_nip04) - SignerType.NIP44_DECRYPT -> stringResource(R.string.decrypt_nip44) - SignerType.NIP04_DECRYPT -> stringResource(R.string.decrypt_nip04) - SignerType.DECRYPT_ZAP_EVENT -> stringResource(R.string.decrypt_zap_event) - else -> stringResource(R.string.encrypt_decrypt) - } Text( - stringResource(R.string.wants_you_to_data, message), + "Event content", fontWeight = FontWeight.Bold ) + + Text( + content, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) } } RawJsonButton( @@ -68,7 +101,21 @@ fun EncryptDecryptData( if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details) ) if (showMore) { - RawJson(onCopy(), Modifier.weight(1f), stringResource(R.string.encrypted_decrypted_data)) + RawJson( + content, + Modifier.weight(1f), + stringResource(R.string.encrypted_decrypted_data) + ) { + clipboardManager.setText(AnnotatedString(onCopy())) + + coroutineScope.launch { + Toast.makeText( + context, + context.getString(R.string.data_copied_to_the_clipboard), + Toast.LENGTH_SHORT + ).show() + } + } } else { Spacer(modifier = Modifier.weight(1f)) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt index 0217e834..859d18d8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt @@ -6,14 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccessTime -import androidx.compose.material.icons.filled.CalendarMonth -import androidx.compose.material.icons.filled.ContentPaste -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.Card -import androidx.compose.material3.Divider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -22,13 +15,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.R -import com.greenart7c3.nostrsigner.models.TimeUtils import com.greenart7c3.nostrsigner.service.model.AmberEvent -import com.greenart7c3.nostrsigner.service.toShortenHex @Composable fun EventData( @@ -36,6 +31,7 @@ fun EventData( remember: MutableState, packageName: String?, appName: String, + applicationName: String?, event: AmberEvent, rawJson: String, onAccept: () -> Unit, @@ -44,65 +40,40 @@ fun EventData( var showMore by androidx.compose.runtime.remember { mutableStateOf(false) } - val eventDescription = stringResource(R.string.event) Column( Modifier .fillMaxSize() .padding(8.dp) ) { - AppTitle(appName) + val text = if (event.kind == 22242) "requests client authentication" else "requests event signature" + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(applicationName ?: appName) + } + append(" $text") + }, + fontSize = 18.sp + ) Spacer(Modifier.size(4.dp)) Card( modifier = Modifier .fillMaxWidth() ) { - Column(Modifier.padding(6.dp)) { - val message = if (event.kind == 22242) { - stringResource(R.string.client_authentication) - } else { - eventDescription - } + Column(Modifier.padding(16.dp)) { Text( - stringResource(R.string.wants_you_to_sign_a, message), + "Event content", fontWeight = FontWeight.Bold ) - - EventRow( - Icons.Default.CalendarMonth, - stringResource(R.string.kind), - "${event.kind}" - ) - - EventRow( - Icons.Default.AccessTime, - stringResource(R.string.created_at), - TimeUtils.format(event.createdAt) - ) - - EventRow( - Icons.Default.Person, - stringResource(R.string.signed_by), - event.pubKey.toShortenHex() - ) - - if (event.kind == 22242) { - EventRow( - Icons.Default.Wifi, - stringResource(R.string.relay), - event.tags.firstOrNull { it.size > 1 && it[0] == "relay" }?.get(1)?.removePrefix("wss://")?.removePrefix("ws://") ?: "" - ) - } else { - EventRow( - Icons.Default.ContentPaste, - stringResource(R.string.content), - event.content - ) - } - - Divider( - modifier = Modifier.padding(top = 15.dp), - thickness = Dp.Hairline + val content = if (event.kind == 22242) event.relay() else event.content + Text( + content, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt index 97e93e34..1d51a52f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt @@ -1,84 +1,252 @@ package com.greenart7c3.nostrsigner.ui.components +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Card +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import com.greenart7c3.nostrsigner.R -import com.greenart7c3.nostrsigner.models.Account -import com.vitorpamplona.quartz.encoders.toNpub +import com.greenart7c3.nostrsigner.models.Permission +import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder @Composable fun LoginWithPubKey( - shouldRunOnAccept: Boolean, - remember: MutableState, - packageName: String?, appName: String, - onAccept: () -> Unit, - onReject: () -> Unit, - account: Account + applicationName: String?, + permissions: List?, + onAccept: (List?) -> Unit, + onReject: () -> Unit ) { - var showMore by androidx.compose.runtime.remember { + var localPermissions by remember { + mutableStateOf(permissions) + } + + var showAdjustDialog by remember { mutableStateOf(false) } + + if (showAdjustDialog) { + var dialogPermissions by remember { + mutableStateOf(localPermissions?.map { it.copy() } ?: listOf()) + } + Dialog( + onDismissRequest = { + showAdjustDialog = false + }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton( + onCancel = { + showAdjustDialog = false + } + ) + + PostButton( + isActive = true, + onPost = { + localPermissions = dialogPermissions.map { + it.copy() + } + showAdjustDialog = false + } + ) + } + + LazyColumn( + Modifier.padding(16.dp) + ) { + items(dialogPermissions.size) { + val permission = dialogPermissions[it] + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + dialogPermissions = dialogPermissions.mapIndexed { j, item -> + if (it == j) { + item.copy(checked = !item.checked) + } else { + item + } + } + } + ) { + Text( + modifier = Modifier.weight(1f), + text = permission.toString() + ) + Switch( + checked = permission.checked, + onCheckedChange = { _ -> + dialogPermissions = dialogPermissions.mapIndexed { j, item -> + if (it == j) { + item.copy(checked = !item.checked) + } else { + item + } + } + } + ) + } + } + } + } + } + } + } + Column( Modifier .fillMaxSize() - .padding(8.dp) + .padding(16.dp) ) { - AppTitle(appName) + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(applicationName ?: appName) + } + append(" would like your permission to read your public key and sign events on your behalf") + }, + fontSize = 18.sp + ) + Spacer(Modifier.size(4.dp)) - Card( - modifier = Modifier - .fillMaxWidth() - ) { - Column( + + localPermissions?.let { + LazyColumn( Modifier - .padding(6.dp) + .fillMaxWidth() + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - stringResource(R.string.wants_to_read_your_public_key), - fontWeight = FontWeight.Bold - ) + itemsIndexed(it.take(3)) { _, item -> + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Icon( + if (item.checked) Icons.Default.Done else Icons.Default.Close, + item.toString(), + tint = if (item.checked) Color.Green else Color.Red + ) + Text(text = item.toString()) + } + } + } + if (it.size > 3) { + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + val isAnyChecked = it.take(3).any { permission -> permission.checked } + Icon( + if (isAnyChecked) Icons.Default.Done else Icons.Default.Close, + "...and more", + tint = if (isAnyChecked) Color.Green else Color.Red + ) + Text(text = "${it.size - 3} more...") + } } } - RawJsonButton( - onCLick = { - showMore = !showMore - }, - if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details) - ) + Spacer(modifier = Modifier.weight(1f)) - if (showMore) { - RawJson(account.keyPair.pubKey.toNpub(), Modifier.weight(1f), stringResource(R.string.my_public_key)) - } else { - Spacer(modifier = Modifier.weight(1f)) + Column( + Modifier + .fillMaxWidth() + .padding(10.dp), + Arrangement.Center, + Alignment.CenterHorizontally + ) { + localPermissions?.let { + if (it.isNotEmpty()) { + TextButton( + onClick = { + showAdjustDialog = true + }, + modifier = Modifier + .padding(8.dp) + ) { + Text(text = "Adjust") + } + } + } + + Button( + modifier = Modifier + .fillMaxWidth(0.8f) + .padding(8.dp), + shape = ButtonBorder, + onClick = { + onAccept(localPermissions) + } + ) { + Text("Grant Permissions") + } + Spacer(modifier = Modifier.width(8.dp)) + Button( + modifier = Modifier + .padding(8.dp), + shape = ButtonBorder, + onClick = onReject, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF5A5554) + ) + ) { + Text(stringResource(R.string.reject)) + } } - - RememberMyChoice( - shouldRunOnAccept, - remember, - packageName, - onAccept - ) - - AcceptRejectButtons( - onAccept, - onReject - ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt index 411152f7..d94b609b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt @@ -21,6 +21,6 @@ fun PostButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: shape = ButtonBorder, contentPadding = PaddingValues(0.dp) ) { - Text(text = "Post", color = Color.White) + Text(text = "Save", color = Color.White) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt index 70b26b45..86086331 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt @@ -18,27 +18,22 @@ import androidx.compose.ui.text.input.TextFieldValue import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder import kotlinx.coroutines.launch -import org.json.JSONObject @Composable fun RawJson( rawJson: String, modifier: Modifier, - label: String = stringResource(R.string.copy_raw_json) + label: String = stringResource(R.string.copy_raw_json), + onCopy: (() -> Unit)? = null ) { val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val clipboardManager = LocalClipboardManager.current - val value = try { - JSONObject(rawJson).toString(2) - } catch (e: Exception) { - rawJson - } OutlinedTextField( modifier = modifier .fillMaxWidth(), - value = TextFieldValue(value), + value = TextFieldValue(rawJson), onValueChange = { }, readOnly = true ) @@ -49,14 +44,18 @@ fun RawJson( Button( shape = ButtonBorder, onClick = { - clipboardManager.setText(AnnotatedString(rawJson)) + if (onCopy != null) { + onCopy() + } else { + clipboardManager.setText(AnnotatedString(rawJson)) - coroutineScope.launch { - Toast.makeText( - context, - context.getString(R.string.data_copied_to_the_clipboard), - Toast.LENGTH_SHORT - ).show() + coroutineScope.launch { + Toast.makeText( + context, + context.getString(R.string.data_copied_to_the_clipboard), + Toast.LENGTH_SHORT + ).show() + } } } ) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt index da8753bc..cc34ef14 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt @@ -3,11 +3,13 @@ package com.greenart7c3.nostrsigner.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder @@ -22,9 +24,12 @@ fun RawJsonButton( .padding(horizontal = 4.dp), contentAlignment = Alignment.Center ) { - Button( + TextButton( shape = ButtonBorder, - onClick = onCLick + onClick = onCLick, + colors = ButtonDefaults.textButtonColors( + contentColor = Color.Black + ) ) { Text(text) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt index fac08759..c556d3a9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt @@ -32,6 +32,7 @@ fun EventDataPreview() { remember, null, "App", + null, event, data, { }, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt index e7113abe..f7ccbba7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt @@ -28,29 +28,29 @@ val Font14SP = 14.sp val Size35dp = 35.dp -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) - val ButtonBorder = RoundedCornerShape(20.dp) val Size20Modifier = Modifier.size(20.dp) +val primaryColor = Color(0xFFF16F26) +val primaryVariant = Color(0xFFC8541A) +val secondaryColor = Color(0xFFFF8E4C) + private val DarkColorPalette = darkColorScheme( - primary = Purple200, - secondary = Purple700, - tertiary = Teal200 + primary = primaryColor, + secondary = primaryVariant, + tertiary = secondaryColor, + primaryContainer = secondaryColor, + secondaryContainer = secondaryColor ) private val LightColorPalette = lightColorScheme( - primary = Purple500, - secondary = Purple700, - tertiary = Teal200 + primary = primaryColor, + secondary = primaryVariant, + tertiary = secondaryColor, + primaryContainer = secondaryColor, + secondaryContainer = secondaryColor ) -private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f) -private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f) - @Composable fun NostrSignerTheme( darkTheme: Boolean = isSystemInDarkTheme(), diff --git a/app/src/main/res/drawable/manage_accounts.xml b/app/src/main/res/drawable/manage_accounts.xml deleted file mode 100644 index 734c7055..00000000 --- a/app/src/main/res/drawable/manage_accounts.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/relays.xml b/app/src/main/res/drawable/relays.xml deleted file mode 100644 index 2ec03a2c..00000000 --- a/app/src/main/res/drawable/relays.xml +++ /dev/null @@ -1,4 +0,0 @@ - - -