core, ui: chat relay test (#6736)

This commit is contained in:
spaced4ndy authored and GitHub committed 2026-04-02 15:36:36 +00:00
1 parent 42fe94752c
commit a14a66db14
40 files changed
+1670 -148

No files matched your search

@@ -2277,11 +2277,16 @@ enum class RelayStatus {
}
}
@Serializable
data class RelayProfile(
val name: String
)
@Serializable
data class UserChatRelay(
val chatRelayId: Long?,
val address: String,
val name: String,
val relayProfile: RelayProfile,
val domains: List<String>,
val preset: Boolean,
val tested: Boolean? = null,
@@ -2291,6 +2296,10 @@ data class UserChatRelay(
@Transient
private val createdAt: Date = Date()
val id: String get() = "$address $createdAt"
val name: String get() = relayProfile.name
fun copyWithName(name: String): UserChatRelay = copy(relayProfile = RelayProfile(name = name))
}
@Serializable
@@ -1216,6 +1216,14 @@ object ChatController {
throw Exception("testProtoServer bad response: ${r.responseType} ${r.details}")
}
suspend fun testChatRelay(rh: Long?, address: String): Pair<RelayProfile?, RelayTestFailure?> {
val userId = currentUserId("testChatRelay")
val r = sendCmd(rh, CC.APITestChatRelay(userId, address))
if (r is API.Result && r.res is CR.ChatRelayTestResult) return r.res.relayProfile to r.res.relayTestFailure
Log.e(TAG, "testChatRelay bad response: ${r.responseType} ${r.details}")
throw Exception("testChatRelay bad response: ${r.responseType} ${r.details}")
}
suspend fun getServerOperators(rh: Long?): ServerOperatorConditionsDetail? {
val r = sendCmd(rh, CC.ApiGetServerOperators())
if (r is API.Result && r.res is CR.ServerOperatorConditions) return r.res.conditions
@@ -3637,6 +3645,7 @@ sealed class CC {
class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC()
class APIAcceptMemberContact(val contactId: Long): CC()
class APITestProtoServer(val userId: Long, val server: String): CC()
class APITestChatRelay(val userId: Long, val address: String): CC()
class ApiGetServerOperators(): CC()
class ApiSetServerOperators(val operators: List<ServerOperator>): CC()
class ApiGetUserServers(val userId: Long): CC()
@@ -3837,6 +3846,7 @@ sealed class CC {
is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}"
is APIAcceptMemberContact -> "/_accept member contact @$contactId"
is APITestProtoServer -> "/_server test $userId $server"
is APITestChatRelay -> "/_relay test $userId $address"
is ApiGetServerOperators -> "/_operators"
is ApiSetServerOperators -> "/_operators ${json.encodeToString(operators)}"
is ApiGetUserServers -> "/_servers $userId"
@@ -4015,6 +4025,7 @@ sealed class CC {
is APISendMemberContactInvitation -> "apiSendMemberContactInvitation"
is APIAcceptMemberContact -> "apiAcceptMemberContact"
is APITestProtoServer -> "testProtoServer"
is APITestChatRelay -> "apiTestChatRelay"
is ApiGetServerOperators -> "apiGetServerOperators"
is ApiSetServerOperators -> "apiSetServerOperators"
is ApiGetUserServers -> "apiGetUserServers"
@@ -4679,6 +4690,44 @@ data class ProtocolTestFailure(
}
}
@Serializable
enum class RelayTestStep {
@SerialName("getLink") GetLink,
@SerialName("decodeLink") DecodeLink,
@SerialName("connect") Connect,
@SerialName("waitResponse") WaitResponse,
@SerialName("verify") Verify;
val text: String get() = when (this) {
GetLink -> generalGetString(MR.strings.relay_test_step_get_link)
DecodeLink -> generalGetString(MR.strings.relay_test_step_decode_link)
Connect -> generalGetString(MR.strings.relay_test_step_connect)
WaitResponse -> generalGetString(MR.strings.relay_test_step_wait_response)
Verify -> generalGetString(MR.strings.relay_test_step_verify)
}
}
@Serializable
data class RelayTestFailure(
val rtfStep: RelayTestStep,
val rtfError: ChatError
) {
val localizedDescription: String get() {
val err = String.format(generalGetString(MR.strings.error_relay_test_failed_at_step), rtfStep.text)
return when {
rtfError is ChatError.ChatErrorAgent &&
rtfError.agentError is AgentErrorType.SMP && rtfError.agentError.smpErr is SMPErrorType.AUTH ->
err + " " + generalGetString(MR.strings.error_relay_test_server_auth)
rtfError is ChatError.ChatErrorAgent &&
rtfError.agentError is AgentErrorType.BROKER && rtfError.agentError.brokerErr is BrokerErrorType.NETWORK &&
rtfError.agentError.brokerErr.networkError is NetworkError.UnknownCAError ->
err + " " + generalGetString(MR.strings.error_smp_test_certificate)
else ->
err + " " + String.format(generalGetString(MR.strings.error_with_info), rtfError.string)
}
}
}
@Serializable
data class ServerAddress(
val serverProtocol: ServerProtocol,
@@ -6206,6 +6255,7 @@ sealed class CR {
@Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List<ChatTag>): CR()
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
@Serializable @SerialName("chatRelayTestResult") class ChatRelayTestResult(val user: UserRef, val relayProfile: RelayProfile? = null, val relayTestFailure: RelayTestFailure? = null): CR()
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
@Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List<UserOperatorServers>): CR()
@Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List<UserServersError>, val serverWarnings: List<UserServersWarning> = emptyList()): CR()
@@ -6393,6 +6443,7 @@ sealed class CR {
is ChatTags -> "chatTags"
is ApiChatItemInfo -> "chatItemInfo"
is ServerTestResult -> "serverTestResult"
is ChatRelayTestResult -> "chatRelayTestResult"
is ServerOperatorConditions -> "serverOperatorConditions"
is UserServers -> "userServers"
is UserServersValidation -> "userServersValidation"
@@ -6572,6 +6623,7 @@ sealed class CR {
is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}")
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
is ChatRelayTestResult -> withUser(user, "relayProfile: $relayProfile\ntestFailure: $relayTestFailure")
is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}"
is UserServers -> withUser(user, "userServers: ${json.encodeToString(userServers)}")
is UserServersValidation -> withUser(user, "serverErrors: ${json.encodeToString(serverErrors)}")
@@ -7179,6 +7231,7 @@ sealed class ChatErrorType {
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited"
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
is RelayTestError -> "relayTestError $message"
is InternalError -> "internalError"
is CEException -> "exception $message"
}
@@ -7260,6 +7313,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
@Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType()
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
@Serializable @SerialName("relayTestError") class RelayTestError(val message: String): ChatErrorType()
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
}
@@ -32,7 +32,8 @@ fun TextEditor(
placeholder: String? = null,
contentPadding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
isValid: (String) -> Boolean = { true },
focusRequester: FocusRequester? = null
focusRequester: FocusRequester? = null,
enabled: Boolean = true
) {
var valid by rememberSaveable { mutableStateOf(true) }
var focused by rememberSaveable { mutableStateOf(false) }
@@ -64,6 +65,7 @@ fun TextEditor(
value = value.value,
onValueChange = { value.value = it },
modifier = if (focusRequester == null) textFieldModifier else textFieldModifier.focusRequester(focusRequester),
enabled = enabled,
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@@ -83,7 +85,7 @@ fun TextEditor(
leadingIcon = null,
trailingIcon = null,
singleLine = false,
enabled = true,
enabled = enabled,
isError = false,
interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
@@ -4,6 +4,7 @@ import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionItemViewSpaceBetween
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
@@ -25,6 +26,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable
fun ShowRelayTestStatus(relay: UserChatRelay, modifier: Modifier = Modifier) =
@@ -115,6 +117,18 @@ fun ChatRelayView(
) {
val relayToEdit = remember { mutableStateOf(relay) }
LaunchedEffect(Unit) {
snapshotFlow { relayToEdit.value.address }
.distinctUntilChanged()
.collect {
if (relayToEdit.value.address == relay.address) {
relayToEdit.value = relayToEdit.value.copy(tested = relay.tested, relayProfile = relay.relayProfile)
} else {
relayToEdit.value = relayToEdit.value.copy(tested = null)
}
}
}
ModalView(
close = {
val validName = validRelayName(relayToEdit.value.name)
@@ -149,25 +163,25 @@ private fun ChatRelayLayout(
relay: MutableState<UserChatRelay>,
onDelete: (() -> Unit)?
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.chat_relay))
if (relay.value.preset) {
PresetRelay(relay)
} else {
CustomRelay(relay, onDelete)
val testing = remember { mutableStateOf(false) }
Box {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.chat_relay))
if (relay.value.preset) {
PresetRelay(relay, testing)
} else {
CustomRelay(relay, onDelete, testing)
}
SectionBottomSpacer()
}
if (testing.value) {
DefaultProgressView(null)
}
SectionBottomSpacer()
}
}
@Composable
private fun PresetRelay(relay: MutableState<UserChatRelay>) {
SectionView(stringResource(MR.strings.preset_relay_name).uppercase()) {
SectionItemView {
Text(relay.value.name)
}
}
SectionDividerSpaced()
private fun PresetRelay(relay: MutableState<UserChatRelay>, testing: MutableState<Boolean>) {
SectionView(stringResource(MR.strings.preset_relay_address).uppercase()) {
SelectionContainer {
Text(
@@ -178,13 +192,20 @@ private fun PresetRelay(relay: MutableState<UserChatRelay>) {
}
}
SectionDividerSpaced()
UseRelaySection(relay)
SectionView(stringResource(MR.strings.preset_relay_name).uppercase()) {
SectionItemView {
Text(relay.value.name)
}
}
SectionDividerSpaced()
UseRelaySection(relay, testing = testing)
}
@Composable
private fun CustomRelay(
relay: MutableState<UserChatRelay>,
onDelete: (() -> Unit)?
onDelete: (() -> Unit)?,
testing: MutableState<Boolean>
) {
val relayName = remember { mutableStateOf(relay.value.name) }
val relayAddress = remember { mutableStateOf(relay.value.address) }
@@ -194,7 +215,12 @@ private fun CustomRelay(
LaunchedEffect(Unit) {
snapshotFlow { relayName.value }
.distinctUntilChanged()
.collect { relay.value = relay.value.copy(name = it) }
.collect { relay.value = relay.value.copyWithName(it) }
}
LaunchedEffect(Unit) {
snapshotFlow { relay.value.name }
.distinctUntilChanged()
.collect { relayName.value = it }
}
LaunchedEffect(Unit) {
snapshotFlow { relayAddress.value }
@@ -202,6 +228,18 @@ private fun CustomRelay(
.collect { relay.value = relay.value.copy(address = it) }
}
SectionView(
stringResource(MR.strings.your_relay_address).uppercase(),
icon = painterResource(MR.images.ic_error),
iconTint = if (!validAddress.value) MaterialTheme.colors.error else Color.Transparent,
) {
TextEditor(
relayAddress,
Modifier.height(144.dp)
)
}
SectionDividerSpaced(maxTopPadding = true)
Column {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
@@ -224,25 +262,17 @@ private fun CustomRelay(
TextEditor(
relayName,
Modifier,
placeholder = generalGetString(MR.strings.enter_relay_name)
placeholder = generalGetString(MR.strings.enter_relay_name),
enabled = relay.value.tested != true
)
}
}
SectionDividerSpaced(maxTopPadding = true)
SectionView(
stringResource(MR.strings.your_relay_address).uppercase(),
icon = painterResource(MR.images.ic_error),
iconTint = if (!validAddress.value) MaterialTheme.colors.error else Color.Transparent,
) {
TextEditor(
relayAddress,
Modifier.height(144.dp)
)
if (relay.value.tested != true) {
SectionTextFooter(annotatedStringResource(MR.strings.test_relay_to_retrieve_name))
}
SectionDividerSpaced(maxTopPadding = true)
UseRelaySection(relay, validAddress.value)
UseRelaySection(relay, validAddress.value, testing)
if (onDelete != null) {
SectionDividerSpaced()
@@ -257,21 +287,31 @@ private fun CustomRelay(
@Composable
private fun UseRelaySection(
relay: MutableState<UserChatRelay>,
valid: Boolean = true
valid: Boolean = true,
testing: MutableState<Boolean>
) {
val scope = rememberCoroutineScope()
SectionView(stringResource(MR.strings.use_relay).uppercase()) {
SectionItemViewSpaceBetween(
click = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.not_implemented),
text = generalGetString(MR.strings.relay_testing_not_available)
)
testing.value = true
relay.value = relay.value.copy(tested = null)
scope.launch {
val f = testRelayConnection(relay)
if (f != null) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.relay_test_failed_alert),
text = f.localizedDescription
)
}
testing.value = false
}
},
disabled = !valid
disabled = !valid || testing.value
) {
Text(
stringResource(MR.strings.test_relay),
color = if (valid) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary
color = if (valid && !testing.value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary
)
ShowRelayTestStatus(relay.value)
}
@@ -322,12 +362,20 @@ fun ModalData.NewChatRelayView(
val relayToEdit = remember {
mutableStateOf(
UserChatRelay(
chatRelayId = null, address = "", name = "", domains = emptyList(),
chatRelayId = null, address = "", relayProfile = RelayProfile(name = ""), domains = emptyList(),
preset = false, tested = null, enabled = true, deleted = false
)
)
}
LaunchedEffect(Unit) {
snapshotFlow { relayToEdit.value.address }
.distinctUntilChanged()
.collect {
relayToEdit.value = relayToEdit.value.copy(tested = null)
}
}
ModalView(close = {
addChatRelay(relayToEdit.value, userServers, serverErrors, serverWarnings, rhId, close)
}) {
@@ -337,9 +385,33 @@ fun ModalData.NewChatRelayView(
@Composable
private fun NewChatRelayLayout(relay: MutableState<UserChatRelay>) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.new_chat_relay))
CustomRelay(relay, onDelete = null)
SectionBottomSpacer()
val testing = remember { mutableStateOf(false) }
Box {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.new_chat_relay))
CustomRelay(relay, onDelete = null, testing = testing)
SectionBottomSpacer()
}
if (testing.value) {
DefaultProgressView(null)
}
}
}
suspend fun testRelayConnection(relay: MutableState<UserChatRelay>): RelayTestFailure? =
try {
val (relayProfile, testFailure) = chatModel.controller.testChatRelay(chatModel.remoteHostId(), relay.value.address)
if (testFailure != null) {
relay.value = relay.value.copy(tested = false)
testFailure
} else {
relay.value = relay.value.copy(tested = true).let {
if (relayProfile != null) it.copyWithName(relayProfile.name) else it
}
null
}
} catch (e: Exception) {
Log.e(TAG, "testRelayConnection ${e.stackTraceToString()}")
relay.value = relay.value.copy(tested = false)
null
}
@@ -444,21 +444,30 @@ fun OperatorViewLayout(
testing = testing,
smpServers = userServers.value[operatorIndex].smpServers,
xftpServers = userServers.value[operatorIndex].xftpServers,
) { p, l ->
when (p) {
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
chatRelays = userServers.value[operatorIndex].chatRelays,
onUpdate = { p, l ->
when (p) {
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
)
}
}
},
onUpdateRelays = { relays ->
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
chatRelays = relays
)
}
}
}
)
}
SectionBottomSpacer()
@@ -203,21 +203,30 @@ fun YourServersViewLayout(
testing = testing,
smpServers = userServers.value[operatorIndex].smpServers,
xftpServers = userServers.value[operatorIndex].xftpServers,
) { p, l ->
when (p) {
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
chatRelays = userServers.value[operatorIndex].chatRelays,
onUpdate = { p, l ->
when (p) {
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
)
}
}
},
onUpdateRelays = { relays ->
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
chatRelays = relays
)
}
}
}
)
HowToButton()
}
@@ -229,16 +238,20 @@ fun YourServersViewLayout(
fun TestServersButton(
smpServers: List<UserServer>,
xftpServers: List<UserServer>,
chatRelays: List<UserChatRelay> = emptyList(),
testing: MutableState<Boolean>,
onUpdate: (ServerProtocol, List<UserServer>) -> Unit
onUpdate: (ServerProtocol, List<UserServer>) -> Unit,
onUpdateRelays: ((List<UserChatRelay>) -> Unit)? = null
) {
val scope = rememberCoroutineScope()
val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled }) || testing.value }
val disabled = derivedStateOf {
(smpServers.none { it.enabled } && xftpServers.none { it.enabled } && chatRelays.filter { !it.deleted }.none { it.enabled }) || testing.value
}
SectionItemView(
{
scope.launch {
testServers(testing, smpServers, xftpServers, chatModel, onUpdate)
testServers(testing, smpServers, xftpServers, chatRelays, chatModel, onUpdate, onUpdateRelays)
}
},
disabled = disabled.value
@@ -338,20 +351,28 @@ private suspend fun testServers(
testing: MutableState<Boolean>,
smpServers: List<UserServer>,
xftpServers: List<UserServer>,
chatRelays: List<UserChatRelay>,
m: ChatModel,
onUpdate: (ServerProtocol, List<UserServer>) -> Unit
onUpdate: (ServerProtocol, List<UserServer>) -> Unit,
onUpdateRelays: ((List<UserChatRelay>) -> Unit)?
) {
val relaysResetStatus = resetRelayTestStatus(chatRelays)
onUpdateRelays?.invoke(relaysResetStatus)
val smpResetStatus = resetTestStatus(smpServers)
onUpdate(ServerProtocol.SMP, smpResetStatus)
val xftpResetStatus = resetTestStatus(xftpServers)
onUpdate(ServerProtocol.XFTP, xftpResetStatus)
testing.value = true
val relayFailures = runRelaysTest(relaysResetStatus) { onUpdateRelays?.invoke(it) }
val smpFailures = runServersTest(smpResetStatus, m) { onUpdate(ServerProtocol.SMP, it) }
val xftpFailures = runServersTest(xftpResetStatus, m) { onUpdate(ServerProtocol.XFTP, it) }
testing.value = false
val fs = smpFailures + xftpFailures
if (fs.isNotEmpty()) {
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
val failures = mutableListOf<String>()
failures += relayFailures.map { (name, f) -> "$name: ${f.localizedDescription}" }
failures += smpFailures.map { (srv, f) -> "$srv: ${f.localizedDescription}" }
failures += xftpFailures.map { (srv, f) -> "$srv: ${f.localizedDescription}" }
if (failures.isNotEmpty()) {
val msg = failures.joinToString("\n")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.smp_servers_test_failed),
text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg
@@ -389,6 +410,37 @@ private suspend fun runServersTest(servers: List<UserServer>, m: ChatModel, onUp
return fs
}
private fun resetRelayTestStatus(relays: List<UserChatRelay>): List<UserChatRelay> {
val copy = ArrayList(relays)
for ((index, relay) in relays.withIndex()) {
if (relay.enabled && !relay.deleted) {
copy.removeAt(index)
copy.add(index, relay.copy(tested = null))
}
}
return copy
}
private suspend fun runRelaysTest(relays: List<UserChatRelay>, onUpdated: (List<UserChatRelay>) -> Unit): Map<String, RelayTestFailure> {
val fs: MutableMap<String, RelayTestFailure> = mutableMapOf()
val updatedRelays = ArrayList<UserChatRelay>(relays)
for ((index, relay) in relays.withIndex()) {
if (relay.enabled && !relay.deleted) {
interruptIfCancelled()
val relayState = mutableStateOf(relay)
val f = testRelayConnection(relayState)
updatedRelays.removeAt(index)
updatedRelays.add(index, relayState.value)
onUpdated(updatedRelays.toList())
if (f != null) {
val name = relayState.value.name.ifEmpty { relayState.value.domains.firstOrNull() ?: relayState.value.address }
fs[name] = f
}
}
}
return fs
}
fun deleteXFTPServer(
userServers: MutableState<List<UserOperatorServers>>,
operatorServersIndex: Int,
@@ -2840,8 +2840,15 @@
<string name="test_relay">Test relay</string>
<string name="use_for_new_channels">Use for new channels</string>
<string name="delete_relay">Delete relay</string>
<string name="not_implemented">Not implemented</string>
<string name="relay_testing_not_available">Relay testing is not yet available.</string>
<string name="test_relay_to_retrieve_name"><![CDATA[<b>Test relay</b> to retrieve its name.]]></string>
<string name="relay_test_failed_alert">Relay test failed!</string>
<string name="relay_test_step_get_link">Get link</string>
<string name="relay_test_step_decode_link">Decode link</string>
<string name="relay_test_step_connect">Connect</string>
<string name="relay_test_step_wait_response">Wait response</string>
<string name="relay_test_step_verify">Verify</string>
<string name="error_relay_test_failed_at_step">Test failed at step %s.</string>
<string name="error_relay_test_server_auth">Server requires authorization to connect to relay, check password.</string>
<string name="invalid_relay_name">Invalid relay name!</string>
<string name="check_relay_name">Check relay name and try again.</string>
<string name="invalid_relay_address">Invalid relay address!</string>