Check if is a valid relay before adding it

This commit is contained in:
greenart7c3
2024-09-11 11:10:36 -03:00
parent 83c5d3ecd9
commit 3e8f435457
6 changed files with 586 additions and 337 deletions
@@ -0,0 +1,102 @@
/**
* 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.util.Log
import com.greenart7c3.nostrsigner.checkNotInMainThread
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import java.io.IOException
import kotlinx.coroutines.CancellationException
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
class Nip11Retriever {
enum class ErrorCode {
FAIL_TO_ASSEMBLE_URL,
FAIL_TO_REACH_SERVER,
FAIL_TO_PARSE_RESULT,
FAIL_WITH_HTTP_STATUS,
}
suspend fun loadRelayInfo(
url: String,
dirtyUrl: String,
onInfo: (Nip11RelayInformation) -> Unit,
onError: (String, ErrorCode, String?) -> Unit,
) {
checkNotInMainThread()
try {
val request: Request =
Request
.Builder()
.header("Accept", "application/nostr+json")
.url(url)
.build()
HttpClientManager
.getHttpClientForUrl(dirtyUrl)
.newCall(request)
.enqueue(
object : Callback {
override fun onResponse(
call: Call,
response: Response,
) {
checkNotInMainThread()
response.use {
val body = it.body.string()
try {
if (it.isSuccessful) {
onInfo(Nip11RelayInformation.fromJson(body))
} else {
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(
"RelayInfoFail",
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
e,
)
onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message)
}
}
}
override fun onFailure(
call: Call,
e: IOException,
) {
Log.e("RelayInfoFail", "$dirtyUrl unavailable", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message)
}
},
)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
}
}
}
@@ -76,93 +76,105 @@ fun AccountScreen(
MainLoginPage(accountStateViewModel)
}
is AccountState.LoggedIn -> {
var isLoading by remember { mutableStateOf(true) }
var isNotificationsConfigured by remember {
mutableStateOf(false)
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
isNotificationsConfigured = LocalPreferences.isNotificationTypeConfigured()
isLoading = false
}
}
if (isNotificationsConfigured) {
var intentData by remember {
mutableStateOf<IntentData?>(null)
}
LaunchedEffect(intent, intents) {
IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) {
intentData = it
}
val data =
intents.firstOrNull {
it.currentAccount.isNotBlank()
}
data?.bunkerRequest?.let {
if (it.currentAccount.isNotBlank()) {
if (LocalPreferences.currentAccount(context) != it.currentAccount) {
accountStateViewModel.switchUser(it.currentAccount, null)
}
}
}
}
if (intentData != null) {
if (intents.none { item -> item.id == intentData!!.id }) {
val oldIntents = intents.toMutableList()
oldIntents.add(intentData!!)
flow.value = oldIntents
}
}
val newIntents =
intents.ifEmpty {
if (intentData == null) {
listOf()
} else {
listOf(intentData)
}
}.mapNotNull { it }
val database = NostrSigner.getInstance().getDatabase(state.account.keyPair.pubKey.toNpub())
val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route)
SideEffect {
NostrSigner.getInstance().applicationIOScope.launch(Dispatchers.IO) {
PushNotificationUtils.accountState = accountStateViewModel
try {
NostrSigner.getInstance().applicationContext.startForegroundService(
Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java),
)
} catch (e: Exception) {
Log.d("NostrSigner", "Failed to start ConnectivityService", e)
}
@Suppress("KotlinConstantConditions")
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
delay(5000)
}
}
}
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database)
} else {
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
if (isLoading) {
Scaffold { innerPadding ->
NotificationTypeScreen(
CenterCircularProgressIndicator(
Modifier
.fillMaxSize()
.padding(innerPadding),
onDone = {
isNotificationsConfigured = true
},
)
}
} else {
if (isNotificationsConfigured) {
var intentData by remember {
mutableStateOf<IntentData?>(null)
}
LaunchedEffect(intent, intents) {
IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) {
intentData = it
}
val data =
intents.firstOrNull {
it.currentAccount.isNotBlank()
}
data?.bunkerRequest?.let {
if (it.currentAccount.isNotBlank()) {
if (LocalPreferences.currentAccount(context) != it.currentAccount) {
accountStateViewModel.switchUser(it.currentAccount, null)
}
}
}
}
if (intentData != null) {
if (intents.none { item -> item.id == intentData!!.id }) {
val oldIntents = intents.toMutableList()
oldIntents.add(intentData!!)
flow.value = oldIntents
}
}
val newIntents =
intents.ifEmpty {
if (intentData == null) {
listOf()
} else {
listOf(intentData)
}
}.mapNotNull { it }
val database = NostrSigner.getInstance().getDatabase(state.account.keyPair.pubKey.toNpub())
val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route)
SideEffect {
NostrSigner.getInstance().applicationIOScope.launch(Dispatchers.IO) {
PushNotificationUtils.accountState = accountStateViewModel
try {
NostrSigner.getInstance().applicationContext.startForegroundService(
Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java),
)
} catch (e: Exception) {
Log.d("NostrSigner", "Failed to start ConnectivityService", e)
}
@Suppress("KotlinConstantConditions")
if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") {
NostrSigner.getInstance().checkForNewRelays()
NotificationDataSource.start()
delay(5000)
}
}
}
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database)
} else {
AmberListenerSingleton.accountStateViewModel = accountStateViewModel
DisplayErrorMessages(accountStateViewModel)
Scaffold { innerPadding ->
NotificationTypeScreen(
Modifier
.fillMaxSize()
.padding(innerPadding),
onDone = {
isNotificationsConfigured = true
},
)
}
}
}
}
}
@@ -122,6 +122,7 @@ fun EditPermission(
if (editRelaysDialog) {
EditRelaysDialog(
applicationData = applicationData,
accountStateViewModel = accountStateViewModel,
onClose = {
editRelaysDialog = false
},
@@ -846,6 +846,7 @@ fun MainScreen(
.fillMaxSize()
.padding(padding),
navController = navController,
accountStateViewModel = accountStateViewModel,
)
}
},
@@ -1,5 +1,6 @@
package com.greenart7c3.nostrsigner.ui.actions
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -26,6 +27,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -56,12 +58,24 @@ import com.greenart7c3.nostrsigner.NostrSigner
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.database.ApplicationEntity
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.service.Nip11Retriever
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
import com.greenart7c3.nostrsigner.ui.components.CloseButton
import com.greenart7c3.nostrsigner.ui.components.PostButton
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayPool
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.signers.NostrSignerInternal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -69,12 +83,18 @@ import kotlinx.coroutines.withContext
@Composable
fun EditRelaysDialog(
applicationData: ApplicationEntity,
accountStateViewModel: AccountStateViewModel,
onClose: () -> Unit,
onPost: (SnapshotStateList<RelaySetupInfo>) -> Unit,
) {
var textFieldRelay by remember {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val textFieldRelay = remember {
mutableStateOf(TextFieldValue(""))
}
val isLoading = remember {
mutableStateOf(false)
}
val relays2 =
remember {
val localRelays = mutableStateListOf<RelaySetupInfo>()
@@ -93,31 +113,244 @@ fun EditRelaysDialog(
modifier = Modifier
.fillMaxSize(),
) {
if (isLoading.value) {
CenterCircularProgressIndicator(Modifier)
} else {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton {
onClose()
}
PostButton(
isActive = !applicationData.isConnected,
onPost = {
onPost(relays2)
},
)
}
LazyColumn(
Modifier
.fillMaxWidth(),
) {
items(relays2.size) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
relays2[it].url,
Modifier
.weight(0.9f)
.padding(8.dp),
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
IconButton(
onClick = {
relays2.removeAt(it)
},
) {
Icon(
Icons.Default.Delete,
stringResource(R.string.delete),
)
}
}
}
}
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
enabled = !applicationData.isConnected,
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(horizontal = 16.dp),
value = textFieldRelay.value.text,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
onValueChange = {
textFieldRelay.value = TextFieldValue(it)
},
keyboardActions = KeyboardActions(
onDone = {
if (applicationData.isConnected) return@KeyboardActions
scope.launch(Dispatchers.IO) {
onAddRelay(
textFieldRelay,
isLoading,
relays2,
scope,
accountStateViewModel,
context,
)
}
},
),
label = {
Text("Relay")
},
)
IconButton(
onClick = {
if (applicationData.isConnected) return@IconButton
scope.launch(Dispatchers.IO) {
onAddRelay(
textFieldRelay,
isLoading,
relays2,
scope,
accountStateViewModel,
context,
)
}
},
) {
Icon(
Icons.Default.Add,
null,
)
}
}
}
}
}
}
}
@Composable
fun DefaultRelaysScreen(
modifier: Modifier,
accountStateViewModel: AccountStateViewModel,
navController: NavController,
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val textFieldRelay = remember {
mutableStateOf(TextFieldValue(""))
}
val isLoading = remember {
mutableStateOf(false)
}
val relays2 =
remember {
val localRelays = mutableStateListOf<RelaySetupInfo>()
NostrSigner.getInstance().settings.defaultRelays.forEach {
localRelays.add(
it.copy(),
)
}
localRelays
}
Surface(
modifier = modifier
.fillMaxSize(),
) {
if (isLoading.value) {
CenterCircularProgressIndicator(Modifier)
} else {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize(),
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(horizontal = 16.dp),
value = textFieldRelay.value.text,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
onValueChange = {
textFieldRelay.value = TextFieldValue(it)
},
keyboardActions = KeyboardActions(
onDone = {
scope.launch(Dispatchers.IO) {
onAddRelay(
textFieldRelay,
isLoading,
relays2,
scope,
accountStateViewModel,
context,
)
}
},
),
label = {
Text("Relay")
},
)
IconButton(
onClick = {
scope.launch(Dispatchers.IO) {
onAddRelay(
textFieldRelay,
isLoading,
relays2,
scope,
accountStateViewModel,
context,
)
}
},
) {
Icon(
Icons.Default.Add,
null,
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton {
onClose()
}
PostButton(
isActive = !applicationData.isConnected,
isActive = relays2.isNotEmpty(),
onPost = {
onPost(relays2)
scope.launch(Dispatchers.IO) {
if (relays2.isNotEmpty()) {
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
defaultRelays = relays2,
)
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
}
scope.launch(Dispatchers.Main) {
navController.navigateUp()
}
}
},
)
}
LazyColumn(
Modifier
.fillMaxWidth(),
.weight(1f),
) {
items(relays2.size) {
Row(
@@ -146,42 +379,103 @@ fun EditRelaysDialog(
}
}
}
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
enabled = !applicationData.isConnected,
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(horizontal = 16.dp),
value = textFieldRelay.text,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
onValueChange = {
textFieldRelay = TextFieldValue(it)
},
keyboardActions = KeyboardActions(
onDone = {
if (applicationData.isConnected) return@KeyboardActions
val url = textFieldRelay.text
if (url.isNotBlank() && url != "/") {
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
if (relays2.any { it.url == addedWSS }) {
textFieldRelay = TextFieldValue("")
return@KeyboardActions
}
}
}
}
}
private suspend fun onAddRelay(
textFieldRelay: MutableState<TextFieldValue>,
isLoading: MutableState<Boolean>,
relays2: SnapshotStateList<RelaySetupInfo>,
scope: CoroutineScope,
accountStateViewModel: AccountStateViewModel,
context: Context,
) {
val url = textFieldRelay.value.text
if (url.isNotBlank() && url != "/") {
isLoading.value = true
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
// TODO: How to identify relays on the local network?
val isLocalHost = url.contains("127.0.0.1") || url.contains("localhost")
if (url.endsWith(".onion") || url.endsWith(".onion/") || isLocalHost) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
if (relays2.any { it.url == addedWSS }) {
textFieldRelay.value = TextFieldValue("")
isLoading.value = false
return
}
val httpsUrl = RelayUrlFormatter.getHttpsUrl(addedWSS)
val retriever = Nip11Retriever()
scope.launch(Dispatchers.IO) {
retriever.loadRelayInfo(
httpsUrl,
addedWSS,
onInfo = { info ->
scope.launch(Dispatchers.IO) secondLaunch@{
if (info.limitation?.payment_required == true) {
accountStateViewModel.toast(
context.getString(R.string.relay),
context.getString(R.string.paid_relays_are_not_supported),
)
textFieldRelay.value = TextFieldValue("")
isLoading.value = false
return@secondLaunch
}
if (info.limitation?.auth_required == true) {
accountStateViewModel.toast(
context.getString(R.string.relay),
context.getString(R.string.auth_required_message),
)
textFieldRelay.value = TextFieldValue("")
isLoading.value = false
return@secondLaunch
}
val signer = NostrSignerInternal(KeyPair())
val encryptedContent = signer.signerSync.nip04Encrypt(
"Test bunker event",
signer.keyPair.pubKey.toHexKey(),
)
encryptedContent?.let {
val event = signer.signerSync.sign<Event>(
TimeUtils.now(),
24133,
arrayOf(arrayOf("p", signer.keyPair.pubKey.toHexKey())),
it,
)
event?.let { signedEvent ->
val relay = Relay(
addedWSS,
read = true,
write = true,
activeTypes = setOf(),
)
RelayPool.addRelay(
relay,
)
relay.connect()
delay(2000)
val result = Client.sendAndWaitForResponse(
signedEvent = signedEvent,
relayList = listOf(RelaySetupInfo(addedWSS, read = true, write = true, setOf())),
)
RelayPool.getRelay(addedWSS)?.disconnect()
RelayPool.removeRelay(
relay,
)
if (result) {
relays2.add(
RelaySetupInfo(
addedWSS,
@@ -190,232 +484,58 @@ fun EditRelaysDialog(
feedTypes = COMMON_FEED_TYPES,
),
)
textFieldRelay = TextFieldValue("")
}
},
),
label = {
Text("Relay")
},
)
IconButton(
onClick = {
if (applicationData.isConnected) return@IconButton
val url = textFieldRelay.text
if (url.isNotBlank() && url != "/") {
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
if (relays2.any { it.url == addedWSS }) {
textFieldRelay = TextFieldValue("")
return@IconButton
}
relays2.add(
RelaySetupInfo(
addedWSS,
read = true,
write = true,
feedTypes = COMMON_FEED_TYPES,
),
)
textFieldRelay = TextFieldValue("")
}
},
) {
Icon(
Icons.Default.Add,
null,
)
}
}
}
}
}
}
@Composable
fun DefaultRelaysScreen(
modifier: Modifier,
navController: NavController,
) {
val scope = rememberCoroutineScope()
var textFieldRelay by remember {
mutableStateOf(TextFieldValue(""))
}
val relays2 =
remember {
val localRelays = mutableStateListOf<RelaySetupInfo>()
NostrSigner.getInstance().settings.defaultRelays.forEach {
localRelays.add(
it.copy(),
)
}
localRelays
}
Surface(
modifier = modifier
.fillMaxSize(),
) {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize(),
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(horizontal = 16.dp),
value = textFieldRelay.text,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
onValueChange = {
textFieldRelay = TextFieldValue(it)
},
keyboardActions = KeyboardActions(
onDone = {
val url = textFieldRelay.text
if (url.isNotBlank() && url != "/") {
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
if (relays2.any { it.url == addedWSS }) {
textFieldRelay = TextFieldValue("")
return@KeyboardActions
}
relays2.add(
RelaySetupInfo(
addedWSS,
read = true,
write = true,
feedTypes = COMMON_FEED_TYPES,
),
)
textFieldRelay = TextFieldValue("")
}
},
),
label = {
Text("Relay")
},
)
IconButton(
onClick = {
val url = textFieldRelay.text
if (url.isNotBlank() && url != "/") {
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
// TODO: How to identify relays on the local network?
val isLocalHost = url.contains("127.0.0.1") || url.contains("localhost")
if (url.endsWith(".onion") || url.endsWith(".onion/") || isLocalHost) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
accountStateViewModel.toast(
context.getString(R.string.relay),
context.getString(R.string.failed_to_send_event),
)
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
if (relays2.any { it.url == addedWSS }) {
textFieldRelay = TextFieldValue("")
return@IconButton
textFieldRelay.value = TextFieldValue("")
}
relays2.add(
RelaySetupInfo(
addedWSS,
read = true,
write = true,
feedTypes = COMMON_FEED_TYPES,
),
)
textFieldRelay = TextFieldValue("")
}
},
) {
Icon(
Icons.Default.Add,
null,
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
PostButton(
isActive = relays2.isNotEmpty(),
onPost = {
scope.launch(Dispatchers.IO) {
if (relays2.isNotEmpty()) {
NostrSigner.getInstance().settings = NostrSigner.getInstance().settings.copy(
defaultRelays = relays2,
)
LocalPreferences.saveSettingsToEncryptedStorage(NostrSigner.getInstance().settings)
}
scope.launch(Dispatchers.Main) {
navController.navigateUp()
}
}
},
)
}
LazyColumn(
Modifier
.weight(1f),
) {
items(relays2.size) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
relays2[it].url,
Modifier
.weight(0.9f)
.padding(8.dp),
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
IconButton(
onClick = {
relays2.removeAt(it)
},
) {
Icon(
Icons.Default.Delete,
stringResource(R.string.delete),
)
isLoading.value = false
}
}
}
}
},
onError = { dirtyUrl, errorCode, exceptionMessage ->
isLoading.value = false
val msg =
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
context.getString(
R.string.relay_information_document_error_assemble_url,
dirtyUrl,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
context.getString(
R.string.relay_information_document_error_assemble_url,
dirtyUrl,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
context.getString(
R.string.relay_information_document_error_assemble_url,
dirtyUrl,
exceptionMessage,
)
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
context.getString(
R.string.relay_information_document_error_assemble_url,
dirtyUrl,
exceptionMessage,
)
}
accountStateViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg,
)
textFieldRelay.value = TextFieldValue("")
},
)
}
}
}
+13
View File
@@ -368,4 +368,17 @@
<string name="direct_notifications_explainer">Keep the relays connections open all the time. Using this option is faster and will result in a better experience</string>
<string name="notification_settings">Notification Settings</string>
<string name="please_configure_your_notification_settings_before_continuing">Please configure your notification settings before continuing.</string>
<string name="relay">Relay</string>
<string name="paid_relays_are_not_supported">Paid relays are not supported</string>
<string name="auth_required_message">Relays that require auth are not supported</string>
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_assemble_url">Failed to assemble NIP-11 url for %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_reach_server">Failed to reach %1$s: %2$s</string>
<string name="relay_information_document_error_failed_to_parse_response">Failed to parse response from %1$s: %2$s</string>
<string name="relay_information_document_error_failed_with_http">Relay declined request %1$s: %2$s</string>
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
<string name="relay_information_document_error_parse_result">Failed to parse result from %1$s: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s failed with code %2$s</string>
<string name="unable_to_download_relay_document">Download relay document unavailable</string>
<string name="failed_to_send_event">Failed to send event to relay</string>
</resources>