mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 03:18:23 +00:00
add support for tor, fix account name not updating
This commit is contained in:
@@ -4,6 +4,10 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<queries>
|
||||
<package android:name="org.torproject.android"/>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".nostrsigner">
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.greenart7c3.nostrsigner.database.AppDatabase
|
||||
import com.greenart7c3.nostrsigner.database.ApplicationEntity
|
||||
import com.greenart7c3.nostrsigner.database.ApplicationPermissionsEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
@@ -34,6 +35,8 @@ private object PrefKeys {
|
||||
const val REMEMBER_APPS = "remember_apps"
|
||||
const val ACCOUNT_NAME = "account_name"
|
||||
const val RATIONALE = "rationale"
|
||||
const val USE_PROXY = "use_proxy"
|
||||
const val PROXY_PORT = "proxy_port"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@@ -202,6 +205,8 @@ object LocalPreferences {
|
||||
prefs.edit().apply {
|
||||
account.keyPair.privKey.let { putString(PrefKeys.NOSTR_PRIVKEY, it?.toHexKey()) }
|
||||
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
putBoolean(PrefKeys.USE_PROXY, account.useProxy)
|
||||
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
|
||||
putString(PrefKeys.ACCOUNT_NAME, account.name)
|
||||
}.apply()
|
||||
}
|
||||
@@ -214,6 +219,9 @@ object LocalPreferences {
|
||||
encryptedPreferences(npub).edit().apply {
|
||||
putString(PrefKeys.ACCOUNT_NAME, value)
|
||||
}.apply()
|
||||
accountCache.get(npub)?.let {
|
||||
it.name = value
|
||||
}
|
||||
}
|
||||
|
||||
fun getAccountName(npub: String): String {
|
||||
@@ -282,9 +290,15 @@ object LocalPreferences {
|
||||
remove(PrefKeys.REMEMBER_APPS)
|
||||
}.apply()
|
||||
val name = getString(PrefKeys.ACCOUNT_NAME, "") ?: ""
|
||||
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
||||
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
||||
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
HttpClientManager.setDefaultProxy(proxy)
|
||||
val account = Account(
|
||||
keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
name = name
|
||||
name = name,
|
||||
useProxy = useProxy,
|
||||
proxyPort = proxyPort
|
||||
)
|
||||
accountCache.put(npub, account)
|
||||
return account
|
||||
|
||||
@@ -12,7 +12,9 @@ import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
class Account(
|
||||
val keyPair: KeyPair,
|
||||
val signer: NostrSigner = NostrSignerInternal(keyPair),
|
||||
var name: String
|
||||
var name: String,
|
||||
var useProxy: Boolean,
|
||||
var proxyPort: Int
|
||||
) {
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ object HttpClientManager {
|
||||
proxy: Proxy?,
|
||||
timeout: Duration
|
||||
): OkHttpClient {
|
||||
Log.d("HttpClient", "Building client with proxy: ${proxy != null}")
|
||||
val seconds = if (proxy != null) timeout.seconds * 2 else timeout.seconds
|
||||
val duration = Duration.ofSeconds(seconds)
|
||||
return OkHttpClient.Builder()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
||||
object PackageUtils {
|
||||
private fun isPackageInstalled(
|
||||
context: Context,
|
||||
target: String
|
||||
): Boolean {
|
||||
return context.packageManager.getInstalledApplications(0).find { info ->
|
||||
info.packageName == target
|
||||
} != null
|
||||
}
|
||||
|
||||
fun isOrbotInstalled(context: Context): Boolean {
|
||||
return isPackageInstalled(context, "org.torproject.android")
|
||||
}
|
||||
|
||||
fun isExternalSignerInstalled(context: Context): Boolean {
|
||||
val intent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse("nostrsigner:")
|
||||
}
|
||||
val infos = context.packageManager.queryIntentActivities(intent, 0)
|
||||
return infos.size > 0
|
||||
}
|
||||
}
|
||||
@@ -61,23 +61,23 @@ class AccountStateViewModel(npub: String?) : ViewModel() {
|
||||
tryLoginExistingAccount(route, npub)
|
||||
}
|
||||
|
||||
fun startUI(key: String, password: String, route: String?) {
|
||||
fun startUI(key: String, password: String, route: String?, useProxy: Boolean, proxyPort: Int) {
|
||||
val account = if (key.startsWith("ncryptsec")) {
|
||||
val newKey = CryptoUtils.decryptNIP49(key, password)
|
||||
?: throw Exception("Could not decrypt key with provided password")
|
||||
Account(KeyPair(Hex.decode(newKey)), name = "")
|
||||
Account(KeyPair(Hex.decode(newKey)), name = "", useProxy = useProxy, proxyPort = proxyPort)
|
||||
} else if (key.startsWith("nsec")) {
|
||||
Account(KeyPair(privKey = key.bechToBytes()), name = "")
|
||||
Account(KeyPair(privKey = key.bechToBytes()), name = "", useProxy = useProxy, proxyPort = proxyPort)
|
||||
} else {
|
||||
Account(KeyPair(Hex.decode(key)), name = "")
|
||||
Account(KeyPair(Hex.decode(key)), name = "", useProxy = useProxy, proxyPort = proxyPort)
|
||||
}
|
||||
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account, route)
|
||||
}
|
||||
|
||||
fun newKey() {
|
||||
val account = Account(KeyPair(), name = "")
|
||||
fun newKey(useProxy: Boolean, proxyPort: Int) {
|
||||
val account = Account(KeyPair(), name = "", useProxy = useProxy, proxyPort = proxyPort)
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account, null)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -21,6 +22,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -32,6 +34,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
@@ -52,8 +55,12 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.service.PackageUtils
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ConnectOrbotDialog
|
||||
import com.greenart7c3.nostrsigner.ui.theme.Size35dp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
@@ -73,6 +80,10 @@ fun LoginPage(
|
||||
key.value.text.startsWith("ncryptsec1")
|
||||
}
|
||||
}
|
||||
val useProxy = remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf("9050") }
|
||||
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -187,7 +198,7 @@ fun LoginPage(
|
||||
|
||||
if (key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
try {
|
||||
accountViewModel.startUI(key.value.text, password.value.text, null)
|
||||
accountViewModel.startUI(key.value.text, password.value.text, null, useProxy = useProxy.value, proxyPort = proxyPort.value.toInt())
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
@@ -273,7 +284,7 @@ fun LoginPage(
|
||||
|
||||
if (key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
try {
|
||||
accountViewModel.startUI(key.value.text, password.value.text, null)
|
||||
accountViewModel.startUI(key.value.text, password.value.text, null, useProxy = useProxy.value, proxyPort = proxyPort.value.toInt())
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
@@ -286,6 +297,46 @@ fun LoginPage(
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
if (BuildConfig.FLAVOR != "offline" && PackageUtils.isOrbotInstalled(context)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = useProxy.value,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
connectOrbotDialogOpen = true
|
||||
} else {
|
||||
useProxy.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Text("Connect through your Orbot setup")
|
||||
}
|
||||
|
||||
if (connectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
proxyPort
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -299,7 +350,7 @@ fun LoginPage(
|
||||
|
||||
if (key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
try {
|
||||
accountViewModel.startUI(key.value.text, password.value.text, null)
|
||||
accountViewModel.startUI(key.value.text, password.value.text, null, useProxy = useProxy.value, proxyPort = proxyPort.value.toInt())
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
@@ -322,7 +373,7 @@ fun LoginPage(
|
||||
.padding(30.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
accountViewModel.newKey()
|
||||
accountViewModel.newKey(useProxy.value, proxyPort.value.toInt())
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.generate_a_new_key))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -8,9 +9,12 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -29,8 +33,10 @@ import androidx.compose.ui.unit.sp
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.service.HttpClientManager
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ConnectOrbotDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.LogoutDialog
|
||||
import com.greenart7c3.nostrsigner.ui.components.HyperlinkText
|
||||
import com.greenart7c3.nostrsigner.ui.components.IconRow
|
||||
@@ -48,6 +54,10 @@ fun SettingsScreen(
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
var shouldShowBottomSheet by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
var checked by remember { mutableStateOf(account.useProxy) }
|
||||
var disconnectTorDialog by remember { mutableStateOf(false) }
|
||||
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf(account.proxyPort.toString()) }
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
confirmValueChange = { it != SheetValue.PartiallyExpanded },
|
||||
@@ -114,6 +124,32 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
IconRow(
|
||||
title =
|
||||
if (checked) {
|
||||
stringResource(R.string.disconnect_from_your_orbot_setup)
|
||||
} else {
|
||||
stringResource(R.string.connect_via_tor_short)
|
||||
},
|
||||
icon = R.drawable.ic_tor,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onLongClick = {
|
||||
conectOrbotDialogOpen = true
|
||||
},
|
||||
onClick = {
|
||||
if (checked) {
|
||||
disconnectTorDialog = true
|
||||
} else {
|
||||
conectOrbotDialogOpen = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
HyperlinkText(
|
||||
@@ -135,6 +171,60 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (conectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { conectOrbotDialogOpen = false },
|
||||
onPost = {
|
||||
conectOrbotDialogOpen = false
|
||||
disconnectTorDialog = false
|
||||
checked = true
|
||||
account.proxyPort = proxyPort.value.toInt()
|
||||
account.useProxy = checked
|
||||
val proxy = HttpClientManager.initProxy(account.useProxy, "127.0.0.1", account.proxyPort)
|
||||
HttpClientManager.setDefaultProxy(proxy)
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.could_not_connect_to_tor),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
},
|
||||
proxyPort
|
||||
)
|
||||
}
|
||||
|
||||
if (disconnectTorDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) },
|
||||
text = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_text)) },
|
||||
onDismissRequest = { disconnectTorDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
disconnectTorDialog = false
|
||||
checked = false
|
||||
account.proxyPort = proxyPort.value.toInt()
|
||||
account.useProxy = checked
|
||||
val proxy = HttpClientManager.initProxy(account.useProxy, "127.0.0.1", account.proxyPort)
|
||||
HttpClientManager.setDefaultProxy(proxy)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { disconnectTorDialog = false }
|
||||
) {
|
||||
Text(text = stringResource(R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
|
||||
@@ -69,9 +69,11 @@ import com.greenart7c3.nostrsigner.ui.QrCodeDrawer
|
||||
import com.greenart7c3.nostrsigner.ui.components.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.greenart7c3.nostrsigner.ui.theme.Size35dp
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
@@ -111,12 +113,17 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Material3RichText(
|
||||
val content = stringResource(R.string.account_backup_tips_md)
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults()
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips_md)
|
||||
)
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
@@ -126,12 +133,17 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
Material3RichText(
|
||||
val content1 = stringResource(R.string.account_backup_tips3_md)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults()
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips3_md)
|
||||
)
|
||||
BasicMarkdown(astNode1)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.greenart7c3.nostrsigner.ui.actions
|
||||
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
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.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.ui.components.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.greenart7c3.nostrsigner.ui.theme.RichTextDefaults
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Composable
|
||||
fun ConnectOrbotDialog(
|
||||
onClose: () -> Unit,
|
||||
onPost: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
portNumber: MutableState<String>
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CloseButton {
|
||||
onClose()
|
||||
}
|
||||
|
||||
val toastMessage = stringResource(R.string.invalid_port_number)
|
||||
|
||||
UseOrbotButton(
|
||||
onPost = {
|
||||
try {
|
||||
Integer.parseInt(portNumber.value)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(toastMessage)
|
||||
return@UseOrbotButton
|
||||
}
|
||||
|
||||
onPost()
|
||||
},
|
||||
isActive = true
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(30.dp)
|
||||
) {
|
||||
val myMarkDownStyle =
|
||||
RichTextDefaults.copy(
|
||||
stringStyle =
|
||||
RichTextDefaults.stringStyle?.copy(
|
||||
linkStyle =
|
||||
SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Row {
|
||||
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = myMarkDownStyle,
|
||||
renderer = null
|
||||
) {
|
||||
BasicMarkdown(astNode1)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = portNumber.value,
|
||||
onValueChange = { portNumber.value = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
label = { Text(text = stringResource(R.string.orbot_socks_port)) },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "9050"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UseOrbotButton(
|
||||
onPost: () -> Unit = {},
|
||||
isActive: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onPost()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.use_orbot), color = Color.White)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -50,3 +51,37 @@ fun IconRow(title: String, icon: ImageVector, tint: Color, onClick: () -> Unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit, onLongClick: (() -> Unit)? = null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = tint
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = title,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
@@ -32,6 +34,8 @@ val primaryColor = Color(0xFFF16F26)
|
||||
val primaryVariant = Color(0xFFC8541A)
|
||||
val secondaryColor = Color(0xFFFF8E4C)
|
||||
|
||||
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
||||
|
||||
private val DarkColorPalette = darkColorScheme(
|
||||
primary = primaryColor,
|
||||
onPrimary = Color.White,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<vector android:height="512dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="512dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17.578,12.201c-0.76,-0.692 -1.721,-1.251 -2.704,-1.81 -0.446,-0.246 -1.81,-1.318 -1.34,-2.838l-0.851,-0.358c1.342,-2.078 3.085,-4.134 5.229,-6.056 -1.721,0.581 -3.24,1.476 -4.379,3.062 0.67,-1.407 1.765,-2.793 2.972,-4.201 -1.654,1.185 -3.084,2.525 -3.979,4.313l0.627,-2.503c-0.894,1.608 -1.52,3.24 -1.766,4.871l-1.317,-0.535 -0.223,0.178c1.162,2.078 0.559,3.174 -0.022,3.553 -1.162,0.783 -2.838,1.788 -3.688,2.659 -1.609,1.654 -2.078,3.218 -1.921,5.296 0.157,2.66 2.101,4.873 4.67,5.744 1.14,0.38 2.19,0.424 3.352,0.424 1.877,0 3.799,-0.491 5.207,-1.676a6.551,6.551 0,0 0,2.369 -5.027,6.875 6.875,0 0,0 -2.236,-5.096zM14.025,21.073c-0.09,0.402 -0.38,0.894 -0.737,1.341 0.134,-0.246 0.246,-0.492 0.313,-0.76 0.559,-1.989 0.805,-2.904 0.537,-5.095 -0.045,-0.224 -0.135,-0.938 -0.471,-1.721 -0.468,-1.185 -1.184,-2.303 -1.272,-2.548 -0.157,-0.38 -0.38,-1.989 -0.403,-3.084 0.023,0.938 0.089,2.659 0.335,3.329 0.067,0.225 0.715,1.229 1.185,2.459 0.312,0.849 0.38,1.632 0.446,1.854 0.224,1.007 -0.045,2.705 -0.401,4.313 -0.111,0.581 -0.426,1.252 -0.828,1.766 0.225,-0.313 0.402,-0.715 0.537,-1.185 0.269,-0.938 0.38,-2.145 0.356,-2.905 -0.021,-0.446 -0.222,-1.407 -0.558,-2.278 -0.201,-0.47 -0.492,-0.961 -0.692,-1.297 -0.224,-0.335 -0.224,-1.072 -0.313,-1.921 0.021,0.916 -0.068,1.385 0.156,2.033 0.134,0.379 0.625,0.916 0.759,1.43 0.201,0.693 0.402,1.453 0.381,1.922 0,0.536 -0.022,1.52 -0.269,2.593 -0.157,0.804 -0.515,1.497 -1.095,1.943 0.246,-0.312 0.38,-0.625 0.447,-0.938 0.089,-0.469 0.111,-0.916 0.156,-1.475a5.96,5.96 0,0 0,-0.111 -1.721c-0.179,-0.805 -0.469,-1.608 -0.604,-2.168 0.022,0.626 0.269,1.408 0.381,2.235 0.089,0.604 0.044,1.206 0.021,1.742 -0.021,0.627 -0.223,1.722 -0.492,2.258 -0.268,-0.112 -0.357,-0.269 -0.537,-0.491 -0.223,-0.291 -0.357,-0.604 -0.491,-0.962a5.043,5.043 0,0 1,-0.291 -0.915,3.071 3.071,0 0,1 0.559,-2.213c0.469,-0.671 0.559,-0.716 0.715,-1.497 -0.223,0.692 -0.379,0.759 -0.871,1.341 -0.559,0.647 -0.648,1.586 -0.648,2.346 0,0.313 0.134,0.671 0.246,1.007 0.134,0.356 0.268,0.714 0.447,0.982 0.134,0.223 0.313,0.379 0.469,0.491 -0.581,-0.156 -1.184,-0.379 -1.564,-0.692 -0.938,-0.805 -1.765,-2.167 -1.877,-3.375 -0.089,-0.982 0.804,-2.413 2.078,-3.128 1.073,-0.626 1.318,-1.319 1.542,-2.459 -0.313,0.983 -0.626,1.833 -1.654,2.348 -1.475,0.804 -2.235,2.1 -2.167,3.352 0.112,1.586 0.737,2.682 2.011,3.554 0.291,0.2 0.693,0.401 1.118,0.559 -1.587,-0.381 -1.788,-0.604 -2.324,-1.229 0,-0.045 -0.134,-0.135 -0.134,-0.156 -0.715,-0.805 -1.609,-2.19 -1.922,-3.464 -0.112,-0.447 -0.224,-0.916 -0.089,-1.363 0.581,-2.101 1.854,-2.905 3.128,-3.775 0.313,-0.225 0.626,-0.426 0.916,-0.649 0.715,-0.559 0.894,-2.012 1.05,-2.838 -0.29,1.006 -0.603,2.258 -1.162,2.659 -0.29,0.224 -0.648,0.402 -0.938,0.604 -1.318,0.894 -2.637,1.743 -3.24,3.91 -0.134,0.56 -0.044,0.962 0.089,1.498 0.335,1.317 1.229,2.748 1.989,3.597l0.134,0.135c0.335,0.381 0.76,0.67 1.274,0.871a5.945,5.945 0,0 1,-1.296 -0.469c-2.078,-1.005 -3.463,-3.173 -3.553,-4.939 -0.179,-3.597 1.542,-4.647 3.151,-5.966 0.894,-0.737 2.145,-1.095 2.86,-2.413 0.134,-0.291 0.224,-0.916 0.045,-1.587 -0.067,-0.224 -0.402,-1.028 -0.537,-1.207l1.989,0.872c-0.044,0.938 -0.067,1.698 0.112,2.391 0.2,0.76 1.184,1.854 1.586,3.129 0.783,2.41 0.583,5.561 0.023,8.019z"/>
|
||||
</vector>
|
||||
@@ -95,4 +95,23 @@
|
||||
<string name="and_more">...and more</string>
|
||||
<string name="would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf">" would like your permission to read your public key and sign events on your behalf"</string>
|
||||
<string name="new_app">New App</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Connect through Tor with Orbot
|
||||
\n\n1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Start Orbot
|
||||
\n3. In Orbot, check the Socks port. The default uses 9050
|
||||
\n4. If necessary change the port in Orbot
|
||||
\n5. Configure the Socks port in this screen
|
||||
\n6. Press the Activate button to use Orbot as a proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||
<string name="invalid_port_number">Invalid port number</string>
|
||||
<string name="use_orbot">Use Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Disconnect Tor/Orbot</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot setup</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Disconnect from your Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Your data will be immediately transferred in the regular network</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="could_not_connect_to_tor">Could not connect to Tor</string>
|
||||
</resources>
|
||||
@@ -4,6 +4,10 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<queries>
|
||||
<package android:name="org.torproject.android"/>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".nostrsigner">
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ core = "3.5.3"
|
||||
coreKtx = "1.12.0"
|
||||
coreSplashscreen = "1.0.1"
|
||||
espressoCore = "3.5.1"
|
||||
firebaseBom = "32.8.0"
|
||||
firebaseBom = "32.8.1"
|
||||
firebaseMessaging = "23.4.1"
|
||||
jna = "5.14.0"
|
||||
junit = "4.13.2"
|
||||
@@ -19,7 +19,7 @@ mockk = "1.13.10"
|
||||
nav_version = "2.7.7"
|
||||
quartz = "v0.86.2"
|
||||
compose_ui = "1.6.5"
|
||||
richtextUi = "48702a8ced"
|
||||
richtextUi = "077a2cde64"
|
||||
roomKtx = "2.6.1"
|
||||
securityCryptoKtx = "1.1.0-alpha06"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
|
||||
Reference in New Issue
Block a user