Merge pull request #357 from greenart7c3/main

Add option to setup connection through Orbot
This commit is contained in:
Vitor Pamplona
2023-05-11 08:33:33 -04:00
committed by GitHub
19 changed files with 363 additions and 33 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="org.torproject.android"/>
</queries>
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
@@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
@@ -55,6 +56,8 @@ private object PrefKeys {
const val LATEST_CONTACT_LIST = "latestContactList" const val LATEST_CONTACT_LIST = "latestContactList"
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
} }
@@ -209,6 +212,8 @@ object LocalPreferences {
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
}.apply() }.apply()
} }
@@ -283,6 +288,9 @@ object LocalPreferences {
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val a = Account( val a = Account(
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
@@ -301,7 +309,9 @@ object LocalPreferences {
zapPaymentRequestServer, zapPaymentRequestServer,
hideDeleteRequestDialog, hideDeleteRequestDialog,
hideBlockAlertDialog, hideBlockAlertDialog,
latestContactList latestContactList,
proxy,
proxyPort
) )
return a return a
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
@@ -26,7 +27,7 @@ object ServiceManager {
fun start() { fun start() {
val myAccount = account val myAccount = account
HttpClient.start(account)
if (myAccount != null) { if (myAccount != null) {
Client.connect(myAccount.activeRelays() ?: myAccount.convertLocalRelays()) Client.connect(myAccount.activeRelays() ?: myAccount.convertLocalRelays())
@@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.Utils import nostr.postr.Utils
import java.math.BigDecimal import java.math.BigDecimal
import java.net.Proxy
import java.util.Locale import java.util.Locale
val DefaultChannels = setOf( val DefaultChannels = setOf(
@@ -59,7 +60,9 @@ class Account(
var zapPaymentRequest: Nip47URI? = null, var zapPaymentRequest: Nip47URI? = null,
var hideDeleteRequestDialog: Boolean = false, var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false,
var backupContactList: ContactListEvent? = null var backupContactList: ContactListEvent? = null,
var proxy: Proxy?,
var proxyPort: Int
) { ) {
var transientHiddenUsers: Set<String> = setOf() var transientHiddenUsers: Set<String> = setOf()
@@ -936,7 +939,7 @@ class Account(
fun activeRelays(): Array<Relay>? { fun activeRelays(): Array<Relay>? {
var usersRelayList = userProfile().latestContactList?.relays()?.map { var usersRelayList = userProfile().latestContactList?.relays()?.map {
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
Relay(it.key, it.value.read, it.value.write, localFeedTypes) Relay(it.key, it.value.read, it.value.write, localFeedTypes, proxy)
} ?: return null } ?: return null
// Ugly, but forces nostr.band as the only search-supporting relay today. // Ugly, but forces nostr.band as the only search-supporting relay today.
@@ -946,7 +949,8 @@ class Account(
Constants.forcedRelayForSearch.url, Constants.forcedRelayForSearch.url,
Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.read,
Constants.forcedRelayForSearch.write, Constants.forcedRelayForSearch.write,
Constants.forcedRelayForSearch.feedTypes Constants.forcedRelayForSearch.feedTypes,
proxy
) )
} }
@@ -955,7 +959,7 @@ class Account(
fun convertLocalRelays(): Array<Relay> { fun convertLocalRelays(): Array<Relay> {
return localRelays.map { return localRelays.map {
Relay(it.url, it.read, it.write, it.feedTypes) Relay(it.url, it.read, it.write, it.feedTypes, proxy)
}.toTypedArray() }.toTypedArray()
} }
@@ -0,0 +1,34 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
object HttpClient {
private var proxy: Proxy? = null
fun start(account: Account?) {
this.proxy = account?.proxy
}
fun getHttpClient(): OkHttpClient {
val seconds = if (proxy != null) 20L else 10L
val duration = Duration.ofSeconds(seconds)
return OkHttpClient.Builder()
.proxy(proxy)
.readTimeout(duration)
.connectTimeout(duration)
.writeTimeout(duration)
.build()
}
fun getProxy(): Proxy? {
return proxy
}
fun initProxy(useProxy: Boolean, hostname: String, port: Int): Proxy? {
return if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port)) else null
}
}
@@ -9,12 +9,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
class Nip05Verifier { class Nip05Verifier() {
val client = OkHttpClient.Builder().build() val client = HttpClient.getHttpClient()
fun assembleUrl(nip05address: String): String? { fun assembleUrl(nip05address: String): String? {
val parts = nip05address.trim().split("@") val parts = nip05address.trim().split("@")
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service.lnurl
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -10,14 +11,13 @@ import kotlinx.coroutines.withContext
import nostr.postr.Bech32 import nostr.postr.Bech32
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import java.math.BigDecimal import java.math.BigDecimal
import java.net.URLEncoder import java.net.URLEncoder
class LightningAddressResolver { class LightningAddressResolver() {
val client = OkHttpClient.Builder().build() val client = HttpClient.getHttpClient()
fun assembleUrl(lnaddress: String): String? { fun assembleUrl(lnaddress: String): String? {
val parts = lnaddress.split("@") val parts = lnaddress.split("@")
@@ -4,13 +4,13 @@ import android.util.Log
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.model.RelayAuthEvent import com.vitorpamplona.amethyst.service.model.RelayAuthEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@@ -54,7 +54,7 @@ class RegisterAccounts(
.post(body) .post(body)
.build() .build()
val client = OkHttpClient.Builder().build() val client = HttpClient.getHttpClient()
client.newCall(request).execute() client.newCall(request).execute()
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service.relays package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.EventInterface import com.vitorpamplona.amethyst.service.model.EventInterface
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@@ -91,7 +92,7 @@ object Client : RelayPool.Listener {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun newSporadicRelay(url: String, feedTypes: Set<FeedType>?, onConnected: (Relay) -> Unit, onDone: (() -> Unit)?) { fun newSporadicRelay(url: String, feedTypes: Set<FeedType>?, onConnected: (Relay) -> Unit, onDone: (() -> Unit)?) {
val relay = Relay(url, true, true, feedTypes ?: emptySet()) val relay = Relay(url, true, true, feedTypes ?: emptySet(), HttpClient.getProxy())
RelayPool.addRelay(relay) RelayPool.addRelay(relay)
relay.requestAndWatch { relay.requestAndWatch {
@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.service.relays package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.HttpClient
object Constants { object Constants {
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS) val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
@@ -10,7 +11,7 @@ object Constants {
fun convertDefaultRelays(): Array<Relay> { fun convertDefaultRelays(): Array<Relay> {
return defaultRelays.map { return defaultRelays.map {
Relay(it.url, it.read, it.write, it.feedTypes) Relay(it.url, it.read, it.write, it.feedTypes, HttpClient.getProxy())
}.toTypedArray() }.toTypedArray()
} }
@@ -11,6 +11,8 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import java.net.Proxy
import java.time.Duration
import java.util.Date import java.util.Date
enum class FeedType { enum class FeedType {
@@ -23,9 +25,16 @@ class Relay(
var url: String, var url: String,
var read: Boolean = true, var read: Boolean = true,
var write: Boolean = true, var write: Boolean = true,
var activeTypes: Set<FeedType> = FeedType.values().toSet() var activeTypes: Set<FeedType> = FeedType.values().toSet(),
proxy: Proxy?
) { ) {
val seconds = if (proxy != null) 20L else 10L
val duration = Duration.ofSeconds(seconds)
private val httpClient = OkHttpClient.Builder() private val httpClient = OkHttpClient.Builder()
.proxy(proxy)
.readTimeout(duration)
.connectTimeout(duration)
.writeTimeout(duration)
.followRedirects(true) .followRedirects(true)
.followSslRedirects(true) .followSslRedirects(true)
.build() .build()
@@ -10,6 +10,7 @@ import android.provider.MediaStore
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import okhttp3.* import okhttp3.*
import okio.BufferedSource import okio.BufferedSource
import okio.IOException import okio.IOException
@@ -32,7 +33,7 @@ object ImageSaver {
onSuccess: () -> Any?, onSuccess: () -> Any?,
onError: (Throwable) -> Any? onError: (Throwable) -> Any?
) { ) {
val client = OkHttpClient.Builder().build() val client = HttpClient.getHttpClient()
val request = Request.Builder() val request = Request.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
@@ -6,6 +6,7 @@ import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okio.BufferedSink import okio.BufferedSink
@@ -70,7 +71,7 @@ object ImageUploader {
val fileName = randomChars() val fileName = randomChars()
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
val client = OkHttpClient.Builder().build() val client = HttpClient.getHttpClient()
val requestBody: RequestBody val requestBody: RequestBody
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
@@ -24,7 +25,9 @@ import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ScaffoldState import androidx.compose.material.ScaffoldState
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -45,13 +48,17 @@ import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@@ -100,7 +107,12 @@ fun DrawerContent(
} }
@Composable @Composable
fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) { fun ProfileContent(
baseAccountUser: User,
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState,
navController: NavController
) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val accountUserState by baseAccountUser.live().metadata.observeAsState() val accountUserState by baseAccountUser.live().metadata.observeAsState()
@@ -197,11 +209,17 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
}) })
) { ) {
Row() { Row() {
Text("${accountUserFollows.cachedFollowCount() ?: "--"}", fontWeight = FontWeight.Bold) Text(
"${accountUserFollows.cachedFollowCount() ?: "--"}",
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.following)) Text(stringResource(R.string.following))
} }
Row(modifier = Modifier.padding(start = 10.dp)) { Row(modifier = Modifier.padding(start = 10.dp)) {
Text("${accountUserFollows.cachedFollowerCount() ?: "--"}", fontWeight = FontWeight.Bold) Text(
"${accountUserFollows.cachedFollowerCount() ?: "--"}",
fontWeight = FontWeight.Bold
)
Text(stringResource(R.string.followers)) Text(stringResource(R.string.followers))
} }
} }
@@ -221,6 +239,10 @@ fun ListContent(
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var backupDialogOpen by remember { mutableStateOf(false) } var backupDialogOpen by remember { mutableStateOf(false) }
var checked by remember { mutableStateOf(account.proxy != null) }
val openDialog = remember { mutableStateOf(false) }
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
var proxyPort = remember { mutableStateOf(account.proxyPort.toString()) }
Column(modifier = modifier.fillMaxHeight()) { Column(modifier = modifier.fillMaxHeight()) {
if (accountUser != null) { if (accountUser != null) {
@@ -259,6 +281,21 @@ fun ListContent(
onClick = { backupDialogOpen = true } onClick = { backupDialogOpen = true }
) )
val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor)
IconRow(
title = textTorProxy,
icon = R.drawable.ic_tor,
tint = MaterialTheme.colors.onBackground,
onClick = {
if (checked) {
openDialog.value = true
} else {
conectOrbotDialogOpen = true
}
}
)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
IconRow( IconRow(
@@ -272,6 +309,58 @@ fun ListContent(
if (backupDialogOpen) { if (backupDialogOpen) {
AccountBackupDialog(account, onClose = { backupDialogOpen = false }) AccountBackupDialog(account, onClose = { backupDialogOpen = false })
} }
if (conectOrbotDialogOpen) {
ConnectOrbotDialog(
onClose = { conectOrbotDialogOpen = false },
onPost = {
conectOrbotDialogOpen = false
openDialog.value = false
checked = true
enableTor(account, true, proxyPort)
},
proxyPort
)
}
if (openDialog.value) {
AlertDialog(
text = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor)) },
onDismissRequest = { },
confirmButton = {
TextButton(
onClick = {
openDialog.value = false
checked = false
enableTor(account, false, proxyPort)
}
) {
Text(text = stringResource(R.string.yes))
}
},
dismissButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text(text = stringResource(R.string.no))
}
}
)
}
}
private fun enableTor(
account: Account,
checked: Boolean,
portNumber: MutableState<String>
) {
account.proxyPort = portNumber.value.toInt()
account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort)
LocalPreferences.saveToEncryptedStorage(account)
ServiceManager.pause()
ServiceManager.start()
} }
@Composable @Composable
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -39,21 +40,22 @@ class AccountStateViewModel() : ViewModel() {
} }
} }
fun startUI(key: String) { fun startUI(key: String, useProxy: Boolean, proxyPort: Int) {
val pattern = Pattern.compile(".+@.+\\.[a-z]+") val pattern = Pattern.compile(".+@.+\\.[a-z]+")
val parsed = Nip19.uriToRoute(key) val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray() val pubKeyParsed = parsed?.hex?.hexToByteArray()
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val account = val account =
if (key.startsWith("nsec")) { if (key.startsWith("nsec")) {
Account(Persona(privKey = key.bechToBytes())) Account(Persona(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort)
} else if (pubKeyParsed != null) { } else if (pubKeyParsed != null) {
Account(Persona(pubKey = pubKeyParsed)) Account(Persona(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort)
} else if (pattern.matcher(key).matches()) { } else if (pattern.matcher(key).matches()) {
// Evaluate NIP-5 // Evaluate NIP-5
Account(Persona()) Account(Persona(), proxy = proxy, proxyPort = proxyPort)
} else { } else {
Account(Persona(Hex.decode(key))) Account(Persona(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort)
} }
LocalPreferences.updatePrefsForLogin(account) LocalPreferences.updatePrefsForLogin(account)
@@ -66,8 +68,9 @@ class AccountStateViewModel() : ViewModel() {
tryLoginExistingAccount() tryLoginExistingAccount()
} }
fun newKey() { fun newKey(useProxy: Boolean, proxyPort: Int) {
val account = Account(Persona()) val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
val account = Account(Persona(), proxy = proxy, proxyPort = proxyPort)
// saves to local preferences // saves to local preferences
LocalPreferences.updatePrefsForLogin(account) LocalPreferences.updatePrefsForLogin(account)
startUI(account) startUI(account)
@@ -0,0 +1,120 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.PostButton
import kotlinx.coroutines.launch
@Composable
fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: MutableState<String>) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.padding(10.dp)
.background(MaterialTheme.colors.background)
.fillMaxSize()
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
CloseButton(onCancel = {
onClose()
})
val toastMessage = stringResource(R.string.invalid_port_number)
PostButton(
onPost = {
try {
Integer.parseInt(portNumber.value)
} catch (_: Exception) {
scope.launch {
Toast.makeText(
context,
toastMessage,
Toast.LENGTH_LONG
).show()
}
return@PostButton
}
onPost()
},
isActive = true
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 30.dp)
) {
MaterialRichText(
style = RichTextStyle().resolveDefaults()
) {
Markdown(
content = stringResource(R.string.connect_through_your_orbot_setup_markdown)
)
}
Spacer(modifier = Modifier.height(10.dp))
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",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
)
}
}
}
}
}
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedOff package com.vitorpamplona.amethyst.ui.screen.loggedOff
import android.content.Context
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -38,6 +39,7 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import java.util.* import java.util.*
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@@ -55,6 +57,9 @@ fun LoginPage(
var dialogOpen by remember { var dialogOpen by remember {
mutableStateOf(false) mutableStateOf(false)
} }
val useProxy = remember { mutableStateOf(false) }
val proxyPort = remember { mutableStateOf("9050") }
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
Column( Column(
modifier = Modifier modifier = Modifier
@@ -158,7 +163,7 @@ fun LoginPage(
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onGo = { onGo = {
try { try {
accountViewModel.startUI(key.value.text) accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt())
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = context.getString(R.string.invalid_key) errorMessage = context.getString(R.string.invalid_key)
} }
@@ -221,6 +226,32 @@ fun LoginPage(
} }
} }
if (isPackageInstalled(context, "org.torproject.android")) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = useProxy.value,
onCheckedChange = {
if (it) {
connectOrbotDialogOpen = true
}
}
)
Text(stringResource(R.string.connect_via_tor))
}
if (connectOrbotDialogOpen) {
ConnectOrbotDialog(
onClose = { connectOrbotDialogOpen = false },
onPost = {
connectOrbotDialogOpen = false
useProxy.value = true
},
proxyPort
)
}
}
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) { Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
@@ -237,7 +268,7 @@ fun LoginPage(
if (acceptedTerms.value && key.value.text.isNotBlank()) { if (acceptedTerms.value && key.value.text.isNotBlank()) {
try { try {
accountViewModel.startUI(key.value.text) accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt())
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = context.getString(R.string.invalid_key) errorMessage = context.getString(R.string.invalid_key)
} }
@@ -265,7 +296,7 @@ fun LoginPage(
.fillMaxWidth(), .fillMaxWidth(),
onClick = { onClick = {
if (acceptedTerms.value) { if (acceptedTerms.value) {
accountViewModel.newKey() accountViewModel.newKey(useProxy.value, proxyPort.value.toInt())
} else { } else {
termsAcceptanceIsRequired = termsAcceptanceIsRequired =
context.getString(R.string.acceptance_of_terms_is_required) context.getString(R.string.acceptance_of_terms_is_required)
@@ -280,3 +311,7 @@ fun LoginPage(
) )
} }
} }
fun isPackageInstalled(context: Context, target: String): Boolean {
return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null
}
+4
View File
@@ -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>
+16
View File
@@ -296,6 +296,11 @@
<string name="poll_author_no_vote">Poll authors can\'t vote in their own polls.</string> <string name="poll_author_no_vote">Poll authors can\'t vote in their own polls.</string>
<string name="poll_hashtag" translatable="false">#zappoll</string> <string name="poll_hashtag" translatable="false">#zappoll</string>
<string name="connect_via_tor">Connect through your Orbot setup</string>
<string name="do_you_really_want_to_disable_tor">Do you really want to disconnect from your Orbot setup?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="disconnect_from_your_orbot_setup">Disconnect from your Orbot setup</string>
<string name="hash_verification_passed">Image is the same since the post</string> <string name="hash_verification_passed">Image is the same since the post</string>
<string name="hash_verification_failed">Image has changed. The author might not have seen the change</string> <string name="hash_verification_failed">Image has changed. The author might not have seen the change</string>
@@ -360,6 +365,17 @@
<string name="follow_list_selection">Follow List</string> <string name="follow_list_selection">Follow List</string>
<string name="follow_list_kind3follows">All Follows</string> <string name="follow_list_kind3follows">All Follows</string>
<string name="follow_list_global">Global</string> <string name="follow_list_global">Global</string>
<string name="connect_through_your_orbot_setup_markdown">
## Connect through your Orbot setup
\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. By default it uses the port 9050
\n4. If necessary change the port
\n5. Configure the Socks port in this screen
\n6. Press the post 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="app_notification_channel_id" translatable="false">DefaultChannelID</string> <string name="app_notification_channel_id" translatable="false">DefaultChannelID</string>
<string name="app_notification_private_message" translatable="false">New notification arrived</string> <string name="app_notification_private_message" translatable="false">New notification arrived</string>