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"