diff --git a/app/src/free/AndroidManifest.xml b/app/src/free/AndroidManifest.xml
index 8ec1b21a..16e8864f 100644
--- a/app/src/free/AndroidManifest.xml
+++ b/app/src/free/AndroidManifest.xml
@@ -4,6 +4,10 @@
+
+
+
+
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt
index 1c9a5b88..abd898d9 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt
@@ -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
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 fd46baef..97d05dde 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt
@@ -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)
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/HttpClientManager.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/HttpClientManager.kt
index d5283be4..98cf1405 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/service/HttpClientManager.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/HttpClientManager.kt
@@ -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()
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/PackageUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/PackageUtils.kt
new file mode 100644
index 00000000..a9d79397
--- /dev/null
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/PackageUtils.kt
@@ -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
+ }
+}
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 ad789a2d..969e2fe3 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt
@@ -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)
}
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 d6710a53..5e346bc2 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt
@@ -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))
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt
index 47f91e91..00ea45e3 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt
@@ -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 })
}
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 fd86d1e5..5a4f11b1 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
@@ -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))
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt
new file mode 100644
index 00000000..4f56ad83
--- /dev/null
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt
@@ -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
+) {
+ 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)
+ }
+}
diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt
index 24cf23db..a0015cd4 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt
@@ -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
+ )
+ }
+ }
+}
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 8b4a1dbb..c94b435d 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
@@ -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,
diff --git a/app/src/main/res/drawable/ic_tor.xml b/app/src/main/res/drawable/ic_tor.xml
new file mode 100644
index 00000000..bafee0c1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tor.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e9b2e8c5..e2f94423 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -95,4 +95,23 @@
...and more
" would like your permission to read your public key and sign events on your behalf"
New App
+
+ ## 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
+
+ Orbot Socks Port
+ Invalid port number
+ Use Orbot
+ Disconnect Tor/Orbot
+ Tor/Orbot setup
+ Disconnect from your Orbot/Tor?
+ Your data will be immediately transferred in the regular network
+ Yes
+ No
+ Could not connect to Tor
\ No newline at end of file
diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml
index db04911d..323058e8 100644
--- a/app/src/play/AndroidManifest.xml
+++ b/app/src/play/AndroidManifest.xml
@@ -4,6 +4,10 @@
+
+
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 49f99678..e8f6e742 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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"