new ui, use NostrSignerInternal from quartz

This commit is contained in:
greenart7c3
2023-11-20 16:37:26 -03:00
parent 27ba3c228f
commit a0238da2fd
26 changed files with 519 additions and 405 deletions
@@ -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<History> {
encryptedPreferences(npub).apply {
val historyJson = getString(PrefKeys.HISTORY, "[]") ?: "[]"
val listType = object : TypeToken<List<History>>() {}.type
return Gson().fromJson<List<History>>(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<List<History>>() {}.type
val history = Gson().fromJson<List<History>>(historyJson, listType).toMutableList()
return Account(
keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
name = name,
savedApps = outputMap,
history = history
savedApps = outputMap
)
}
}
@@ -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)
}
}
}
@@ -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<String>?): 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>(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"))
@@ -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<String, Boolean>,
var history: MutableList<History> = mutableListOf()
var savedApps: MutableMap<String, Boolean>
) {
val saveable: AccountLiveData = AccountLiveData(this)
}
@@ -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<Permission>?
)
enum class SignerType {
@@ -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
@@ -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<List<Permission>>() {}.type
val permissions = Gson().fromJson<List<Permission>?>(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
@@ -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()}"
}
}
@@ -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<List<String>>, 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)
}
@@ -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)
}
}
}
@@ -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)
}
@@ -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<Event>(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
)
}
}
@@ -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
)
)
)
}
}
}
@@ -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<Permission>? = 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 }),
@@ -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(
@@ -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))
}
}
}
}
@@ -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<Boolean>,
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))
}
@@ -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<Boolean>,
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)
)
}
}
@@ -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<Boolean>,
packageName: String?,
appName: String,
onAccept: () -> Unit,
onReject: () -> Unit,
account: Account
applicationName: String?,
permissions: List<Permission>?,
onAccept: (List<Permission>?) -> 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
)
}
}
@@ -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)
}
}
@@ -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()
}
}
}
) {
@@ -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)
}
@@ -32,6 +32,7 @@ fun EventDataPreview() {
remember,
null,
"App",
null,
event,
data,
{ },
@@ -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(),
@@ -1,7 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
<path android:fillColor="@android:color/white" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
<path android:fillColor="@android:color/white" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
</vector>
-4
View File
@@ -1,4 +0,0 @@
<vector android:height="120dp" android:viewportHeight="47"
android:viewportWidth="47" android:width="120dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#712F8F" android:fillType="evenOdd" android:pathData="M35,11C37.209,11 39,9.209 39,7C39,4.791 37.209,3 35,3C32.791,3 31,4.791 31,7C31,9.209 32.791,11 35,11ZM35,14C38.866,14 42,10.866 42,7C42,3.134 38.866,0 35,0C31.134,0 28,3.134 28,7C28,8.359 28.387,9.627 29.057,10.7L26.611,13.146C25.019,11.828 22.976,11.035 20.747,11.035C18.586,11.035 16.598,11.781 15.029,13.029L11.259,9.259C11.73,8.448 12,7.505 12,6.5C12,3.462 9.538,1 6.5,1C3.462,1 1,3.462 1,6.5C1,9.538 3.462,12 6.5,12C7.442,12 8.328,11.763 9.103,11.346L13.012,15.255C12.088,16.689 11.552,18.397 11.552,20.23C11.552,22.265 12.213,24.145 13.332,25.669L11.709,27.291C10.509,26.476 9.06,26 7.5,26C3.358,26 0,29.358 0,33.5C0,37.642 3.358,41 7.5,41C11.642,41 15,37.642 15,33.5C15,32.003 14.561,30.608 13.805,29.437L15.477,27.766C16.969,28.812 18.786,29.426 20.747,29.426C22.612,29.426 24.347,28.871 25.796,27.917L30.589,32.711C29.588,34.132 29,35.865 29,37.736C29,42.56 32.911,46.471 37.736,46.471C42.56,46.471 46.471,42.56 46.471,37.736C46.471,32.911 42.56,29 37.736,29C35.865,29 34.132,29.588 32.71,30.589L28.002,25.881C29.218,24.322 29.942,22.361 29.942,20.23C29.942,18.465 29.445,16.817 28.583,15.417L31.152,12.848C32.256,13.576 33.578,14 35,14ZM9,6.5C9,7.881 7.881,9 6.5,9C5.119,9 4,7.881 4,6.5C4,5.119 5.119,4 6.5,4C7.881,4 9,5.119 9,6.5ZM7.5,38C9.985,38 12,35.985 12,33.5C12,31.015 9.985,29 7.5,29C5.015,29 3,31.015 3,33.5C3,35.985 5.015,38 7.5,38ZM43.471,37.736C43.471,40.903 40.903,43.471 37.736,43.471C34.568,43.471 32,40.903 32,37.736C32,34.568 34.568,32 37.736,32C40.903,32 43.471,34.568 43.471,37.736ZM26.942,20.23C26.942,23.652 24.169,26.426 20.747,26.426C17.326,26.426 14.552,23.652 14.552,20.23C14.552,16.809 17.326,14.035 20.747,14.035C24.169,14.035 26.942,16.809 26.942,20.23Z"/>
</vector>