mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-25 14:14:39 +00:00
android: Advanced server config (#1403)
* android: Advanced server config * Update apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt * Camera permission, dropping tested value, different font * For review * Partial redraw of the view in testing stage * Comment * Icon * Icon Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e6e5faeb9c
commit
21722b3417
@@ -44,7 +44,10 @@ class ChatModel(val controller: ChatController) {
|
||||
|
||||
val terminalItems = mutableStateListOf<TerminalItem>()
|
||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||
val userSMPServers = mutableStateOf<(List<String>)?>(null)
|
||||
val userSMPServers = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||
// Allows to temporary save servers that are being edited on multiple screens
|
||||
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||
val presetSMPServers = mutableStateOf<(List<String>)?>(null)
|
||||
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
||||
|
||||
// set when app opened from external intent
|
||||
|
||||
@@ -34,7 +34,7 @@ import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlin.concurrent.thread
|
||||
import java.util.Date
|
||||
|
||||
typealias ChatCtrl = Long
|
||||
|
||||
@@ -235,7 +235,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
apiSetFilesFolder(getAppFilesDirectory(appContext))
|
||||
apiSetIncognito(chatModel.incognito.value)
|
||||
chatModel.userAddress.value = apiGetUserAddress()
|
||||
chatModel.userSMPServers.value = getUserSMPServers()
|
||||
val smpServers = getUserSMPServers()
|
||||
chatModel.userSMPServers.value = smpServers?.first
|
||||
chatModel.presetSMPServers.value = smpServers?.second
|
||||
chatModel.chatItemTTL.value = getChatItemTTL()
|
||||
val chats = apiGetChats()
|
||||
chatModel.updateChats(chats)
|
||||
@@ -425,14 +427,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getUserSMPServers(): List<String>? {
|
||||
private suspend fun getUserSMPServers(): Pair<List<ServerCfg>, List<String>>? {
|
||||
val r = sendCmd(CC.GetUserSMPServers())
|
||||
if (r is CR.UserSMPServers) return r.smpServers
|
||||
if (r is CR.UserSMPServers) return r.smpServers to r.presetSMPServers
|
||||
Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun setUserSMPServers(smpServers: List<String>): Boolean {
|
||||
suspend fun setUserSMPServers(smpServers: List<ServerCfg>): Boolean {
|
||||
val r = sendCmd(CC.SetUserSMPServers(smpServers))
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
@@ -447,6 +449,17 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
|
||||
val r = sendCmd(CC.TestSMPServer(smpServer))
|
||||
return when (r) {
|
||||
is CR.SmpTestResult -> r.smpTestFailure
|
||||
else -> {
|
||||
Log.e(TAG, "testSMPServer bad response: ${r.responseType} ${r.details}")
|
||||
throw Exception("testSMPServer bad response: ${r.responseType} ${r.details}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChatItemTTL(): ChatItemTTL {
|
||||
val r = sendCmd(CC.APIGetChatItemTTL())
|
||||
if (r is CR.ChatItemTTL) return ChatItemTTL.fromSeconds(r.chatItemTTL)
|
||||
@@ -1465,7 +1478,8 @@ sealed class CC {
|
||||
class APIDeleteGroupLink(val groupId: Long): CC()
|
||||
class APIGetGroupLink(val groupId: Long): CC()
|
||||
class GetUserSMPServers: CC()
|
||||
class SetUserSMPServers(val smpServers: List<String>): CC()
|
||||
class SetUserSMPServers(val smpServers: List<ServerCfg>): CC()
|
||||
class TestSMPServer(val smpServer: String): CC()
|
||||
class APISetChatItemTTL(val seconds: Long?): CC()
|
||||
class APIGetChatItemTTL: CC()
|
||||
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
|
||||
@@ -1530,8 +1544,9 @@ sealed class CC {
|
||||
is APICreateGroupLink -> "/_create link #$groupId"
|
||||
is APIDeleteGroupLink -> "/_delete link #$groupId"
|
||||
is APIGetGroupLink -> "/_get link #$groupId"
|
||||
is GetUserSMPServers -> "/smp_servers"
|
||||
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
|
||||
is GetUserSMPServers -> "/smp"
|
||||
is SetUserSMPServers -> "/_smp ${smpServersStr(smpServers)}"
|
||||
is TestSMPServer -> "/smp test $smpServer"
|
||||
is APISetChatItemTTL -> "/_ttl ${chatItemTTLStr(seconds)}"
|
||||
is APIGetChatItemTTL -> "/ttl"
|
||||
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
||||
@@ -1599,6 +1614,7 @@ sealed class CC {
|
||||
is APIGetGroupLink -> "apiGetGroupLink"
|
||||
is GetUserSMPServers -> "getUserSMPServers"
|
||||
is SetUserSMPServers -> "setUserSMPServers"
|
||||
is TestSMPServer -> "testSMPServer"
|
||||
is APISetChatItemTTL -> "apiSetChatItemTTL"
|
||||
is APIGetChatItemTTL -> "apiGetChatItemTTL"
|
||||
is APISetNetworkConfig -> "/apiSetNetworkConfig"
|
||||
@@ -1656,7 +1672,7 @@ sealed class CC {
|
||||
companion object {
|
||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||
|
||||
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ";")
|
||||
fun smpServersStr(smpServers: List<ServerCfg>) = if (smpServers.isEmpty()) "default" else json.encodeToString(SMPServersConfig(smpServers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1687,6 +1703,147 @@ class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? =
|
||||
@Serializable
|
||||
class DBEncryptionConfig(val currentKey: String, val newKey: String)
|
||||
|
||||
@Serializable
|
||||
data class SMPServersConfig(
|
||||
val smpServers: List<ServerCfg>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerCfg(
|
||||
val server: String,
|
||||
val preset: Boolean,
|
||||
val tested: Boolean? = null,
|
||||
val enabled: Boolean
|
||||
) {
|
||||
@Transient
|
||||
private val createdAt: Date = Date()
|
||||
// val sendEnabled: Boolean // can we potentially want to prevent sending on the servers we use to receive?
|
||||
// Even if we don't see the use case, it's probably better to allow it in the model
|
||||
// In any case, "trusted/known" servers are out of scope of this change
|
||||
val id: String
|
||||
get() = "$server $createdAt"
|
||||
|
||||
val isBlank: Boolean
|
||||
get() = server.isBlank()
|
||||
|
||||
companion object {
|
||||
val empty = ServerCfg(server = "", preset = false, tested = null, enabled = true)
|
||||
|
||||
class SampleData(
|
||||
val preset: ServerCfg,
|
||||
val custom: ServerCfg,
|
||||
val untested: ServerCfg
|
||||
)
|
||||
|
||||
val sampleData = SampleData(
|
||||
preset = ServerCfg(
|
||||
server = "smp://abcd@smp8.simplex.im",
|
||||
preset = true,
|
||||
tested = true,
|
||||
enabled = true
|
||||
),
|
||||
custom = ServerCfg(
|
||||
server = "smp://abcd@smp9.simplex.im",
|
||||
preset = false,
|
||||
tested = false,
|
||||
enabled = false
|
||||
),
|
||||
untested = ServerCfg(
|
||||
server = "smp://abcd@smp10.simplex.im",
|
||||
preset = false,
|
||||
tested = null,
|
||||
enabled = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SMPTestStep {
|
||||
@SerialName("connect") Connect,
|
||||
@SerialName("createQueue") CreateQueue,
|
||||
@SerialName("secureQueue") SecureQueue,
|
||||
@SerialName("deleteQueue") DeleteQueue,
|
||||
@SerialName("disconnect") Disconnect;
|
||||
|
||||
val text: String get() = when (this) {
|
||||
Connect -> generalGetString(R.string.smp_server_test_connect)
|
||||
CreateQueue -> generalGetString(R.string.smp_server_test_create_queue)
|
||||
SecureQueue -> generalGetString(R.string.smp_server_test_secure_queue)
|
||||
DeleteQueue -> generalGetString(R.string.smp_server_test_delete_queue)
|
||||
Disconnect -> generalGetString(R.string.smp_server_test_disconnect)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SMPTestFailure(
|
||||
val testStep: SMPTestStep,
|
||||
val testError: AgentErrorType
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is SMPTestFailure) return false
|
||||
return other.testStep == this.testStep
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return testStep.hashCode()
|
||||
}
|
||||
|
||||
val localizedDescription: String get() {
|
||||
val err = String.format(generalGetString(R.string.error_smp_test_failed_at_step), testStep.text)
|
||||
return when {
|
||||
testError is AgentErrorType.SMP && testError.smpErr is SMPErrorType.AUTH ->
|
||||
err + " " + generalGetString(R.string.error_smp_test_server_auth)
|
||||
testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK ->
|
||||
err + " " + generalGetString(R.string.error_smp_test_certificate)
|
||||
else -> err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerAddress(
|
||||
val hostnames: List<String>,
|
||||
val port: String,
|
||||
val keyHash: String,
|
||||
val basicAuth: String = ""
|
||||
) {
|
||||
val uri: String
|
||||
get() =
|
||||
"smp://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}"
|
||||
|
||||
val valid: Boolean
|
||||
get() = hostnames.isNotEmpty() && hostnames.toSet().size == hostnames.size
|
||||
|
||||
companion object {
|
||||
val empty = ServerAddress(
|
||||
hostnames = emptyList(),
|
||||
port = "",
|
||||
keyHash = "",
|
||||
basicAuth = ""
|
||||
)
|
||||
val sampleData = ServerAddress(
|
||||
hostnames = listOf("smp.simplex.im", "1234.onion"),
|
||||
port = "",
|
||||
keyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=",
|
||||
basicAuth = "server_password"
|
||||
)
|
||||
|
||||
fun parseServerAddress(s: String): ServerAddress? {
|
||||
val parsed = chatParseServer(s)
|
||||
return runCatching { json.decodeFromString(ParsedServerAddress.serializer(), parsed) }
|
||||
.onFailure { Log.d(TAG, "parseServerAddress decode error: $it") }
|
||||
.getOrNull()?.serverAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ParsedServerAddress (
|
||||
var serverAddress: ServerAddress?,
|
||||
var parseError: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NetCfg(
|
||||
val socksProxy: String? = null,
|
||||
@@ -1828,7 +1985,8 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
|
||||
@Serializable @SerialName("apiChats") class ApiChats(val chats: List<Chat>): CR()
|
||||
@Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR()
|
||||
@Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List<String>): CR()
|
||||
@Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List<ServerCfg>, val presetSMPServers: List<String>): CR()
|
||||
@Serializable @SerialName("smpTestResult") class SmpTestResult(val smpTestFailure: SMPTestFailure? = null): CR()
|
||||
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val chatItemTTL: Long? = null): CR()
|
||||
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
|
||||
@Serializable @SerialName("contactInfo") class ContactInfo(val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR()
|
||||
@@ -1926,6 +2084,7 @@ sealed class CR {
|
||||
is ApiChats -> "apiChats"
|
||||
is ApiChat -> "apiChat"
|
||||
is UserSMPServers -> "userSMPServers"
|
||||
is SmpTestResult -> "smpTestResult"
|
||||
is ChatItemTTL -> "chatItemTTL"
|
||||
is NetworkConfig -> "networkConfig"
|
||||
is ContactInfo -> "contactInfo"
|
||||
@@ -2020,7 +2179,8 @@ sealed class CR {
|
||||
is ChatStopped -> noDetails()
|
||||
is ApiChats -> json.encodeToString(chats)
|
||||
is ApiChat -> json.encodeToString(chat)
|
||||
is UserSMPServers -> json.encodeToString(smpServers)
|
||||
is UserSMPServers -> "$smpServers: ${json.encodeToString(smpServers)}\n$presetSMPServers: ${json.encodeToString(presetSMPServers)}"
|
||||
is SmpTestResult -> json.encodeToString(smpTestFailure)
|
||||
is ChatItemTTL -> json.encodeToString(chatItemTTL)
|
||||
is NetworkConfig -> json.encodeToString(networkConfig)
|
||||
is ContactInfo -> "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}"
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
|
||||
class AlertManager {
|
||||
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
|
||||
@@ -35,6 +41,26 @@ class AlertManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
Dialog(onDismissRequest = this::hideAlert) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
Text(title, Modifier.padding(DEFAULT_PADDING), fontSize = 18.sp)
|
||||
if (text != null) {
|
||||
Text(text)
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
buttons()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertDialog(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -31,6 +32,28 @@ fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(),
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionView(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
iconTint: Color = HighOrLowlight,
|
||||
leadingIcon: Boolean = false,
|
||||
padding: PaddingValues = PaddingValues(),
|
||||
content: (@Composable ColumnScope.() -> Unit)
|
||||
) {
|
||||
Column {
|
||||
val iconSize = with(LocalDensity.current) { 15.sp.toDp() }
|
||||
Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (leadingIcon) Icon(icon, null, Modifier.padding(end = 4.dp).size(iconSize), tint = iconTint)
|
||||
Text(title, color = HighOrLowlight, style = MaterialTheme.typography.body2, fontSize = 12.sp)
|
||||
if (!leadingIcon) Icon(icon, null, Modifier.padding(start = 4.dp).size(iconSize), tint = iconTint)
|
||||
}
|
||||
Surface(color = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background) {
|
||||
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> SectionViewSelectable(
|
||||
title: String?,
|
||||
@@ -56,7 +79,12 @@ fun <T> SectionViewSelectable(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionItemView(click: (() -> Unit)? = null, minHeight: Dp = 46.dp, disabled: Boolean = false, content: (@Composable RowScope.() -> Unit)) {
|
||||
fun SectionItemView(
|
||||
click: (() -> Unit)? = null,
|
||||
minHeight: Dp = 46.dp,
|
||||
disabled: Boolean = false,
|
||||
content: (@Composable RowScope.() -> Unit)
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = minHeight)
|
||||
|
||||
@@ -12,21 +12,28 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
|
||||
@Composable
|
||||
fun TextEditor(modifier: Modifier, text: MutableState<String>) {
|
||||
fun TextEditor(
|
||||
modifier: Modifier,
|
||||
text: MutableState<String>,
|
||||
border: Boolean = true,
|
||||
fontSize: TextUnit = 14.sp,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
onChange: ((String) -> Unit)? = null
|
||||
) {
|
||||
BasicTextField(
|
||||
value = text.value,
|
||||
onValueChange = { text.value = it },
|
||||
onValueChange = { text.value = it; onChange?.invoke(it) },
|
||||
textStyle = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace, fontSize = fontSize,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
@@ -37,17 +44,17 @@ fun TextEditor(modifier: Modifier, text: MutableState<String>) {
|
||||
cursorBrush = SolidColor(HighOrLowlight),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||
shape = if (border) RoundedCornerShape(10.dp) else RectangleShape,
|
||||
border = if (border) BorderStroke(1.dp, MaterialTheme.colors.secondary) else null
|
||||
) {
|
||||
Row(
|
||||
Modifier.background(MaterialTheme.colors.background),
|
||||
Modifier.background(background),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 5.dp, horizontal = 7.dp)
|
||||
.padding(vertical = 5.dp, horizontal = if (border) 7.dp else DEFAULT_PADDING)
|
||||
) {
|
||||
innerTextField()
|
||||
}
|
||||
|
||||
+5
-1
@@ -32,6 +32,10 @@ fun NetworkAndServersView(
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
chatModel.userSMPServersUnsaved.value = null
|
||||
}
|
||||
|
||||
NetworkAndServersLayout(
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
@@ -112,7 +116,7 @@ fun NetworkAndServersView(
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_and_servers))
|
||||
SectionView(generalGetString(R.string.settings_section_title_messages)) {
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) })
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showSettingsModal { SMPServersView(it) })
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
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.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
|
||||
var testing by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
SMPServerLayout(
|
||||
testing,
|
||||
server,
|
||||
testServer = {
|
||||
testing = true
|
||||
scope.launch {
|
||||
val res = testServerConnection(server, m)
|
||||
if (isActive) {
|
||||
onUpdate(res.first)
|
||||
testing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onUpdate,
|
||||
onDelete
|
||||
)
|
||||
if (testing) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServerLayout(
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(if (server.preset) R.string.smp_servers_preset_server else R.string.smp_servers_your_server))
|
||||
|
||||
if (server.preset) {
|
||||
PresetServer(testing, server, testServer, onUpdate, onDelete)
|
||||
} else {
|
||||
CustomServer(testing, server, testServer, onUpdate, onDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PresetServer(
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
SectionView(stringResource(R.string.smp_servers_preset_address).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
server.server,
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
UseServerSection(true, testing, server, testServer, onUpdate, onDelete)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomServer(
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
val serverAddress = remember { mutableStateOf(server.server) }
|
||||
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
|
||||
SectionView(
|
||||
stringResource(R.string.smp_servers_your_server_address).uppercase(),
|
||||
icon = Icons.Outlined.ErrorOutline,
|
||||
iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent,
|
||||
) {
|
||||
val testedPreviously = remember { mutableMapOf<String, Boolean?>() }
|
||||
TextEditor(
|
||||
Modifier.height(144.dp),
|
||||
text = serverAddress,
|
||||
border = false,
|
||||
fontSize = 16.sp,
|
||||
background = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background
|
||||
) {
|
||||
testedPreviously[server.server] = server.tested
|
||||
onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value]))
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete)
|
||||
SectionSpacer()
|
||||
|
||||
if (valid.value) {
|
||||
SectionView(stringResource(R.string.smp_servers_add_to_another_device).uppercase()) {
|
||||
QRCode(serverAddress.value, Modifier.aspectRatio(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseServerSection(
|
||||
valid: Boolean,
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
SectionView(stringResource(R.string.smp_servers_use_server).uppercase()) {
|
||||
SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) {
|
||||
Text(stringResource(R.string.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
ShowTestStatus(server)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
val enabled = rememberUpdatedState(server.enabled)
|
||||
PreferenceToggle(stringResource(R.string.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) }
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView(onDelete, disabled = testing) {
|
||||
Text(stringResource(R.string.smp_servers_delete_server), color = if (testing) HighOrLowlight else MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
|
||||
when (server.tested) {
|
||||
true -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Green)
|
||||
false -> Icon(Icons.Outlined.Clear, null, modifier, tint = MaterialTheme.colors.error)
|
||||
else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent)
|
||||
}
|
||||
|
||||
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, SMPTestFailure?> =
|
||||
try {
|
||||
val r = m.controller.testSMPServer(server.server)
|
||||
server.copy(tested = r == null) to r
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")
|
||||
server.copy(tested = false) to null
|
||||
}
|
||||
|
||||
fun serverHostname(srv: ServerCfg): String =
|
||||
parseServerAddress(srv.server)?.hostnames?.firstOrNull() ?: srv.server
|
||||
@@ -1,258 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionItemViewSpaceBetween
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.OpenInNew
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun SMPServersView(chatModel: ChatModel) {
|
||||
val userSMPServers = chatModel.userSMPServers.value
|
||||
if (userSMPServers != null) {
|
||||
var isUserSMPServers by remember { mutableStateOf(userSMPServers.isNotEmpty()) }
|
||||
var editSMPServers by remember { mutableStateOf(!isUserSMPServers) }
|
||||
val userSMPServersStr = remember { mutableStateOf(if (isUserSMPServers) userSMPServers.joinToString(separator = "\n") else "") }
|
||||
fun saveSMPServers(smpServers: List<String>) {
|
||||
withApi {
|
||||
val r = chatModel.controller.setUserSMPServers(smpServers = smpServers)
|
||||
if (r) {
|
||||
chatModel.userSMPServers.value = smpServers
|
||||
if (smpServers.isEmpty()) {
|
||||
isUserSMPServers = false
|
||||
editSMPServers = true
|
||||
} else {
|
||||
editSMPServers = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SMPServersLayout(
|
||||
isUserSMPServers = isUserSMPServers,
|
||||
editSMPServers = editSMPServers,
|
||||
userSMPServersStr = userSMPServersStr,
|
||||
isUserSMPServersOnOff = { switch ->
|
||||
if (switch) {
|
||||
isUserSMPServers = true
|
||||
} else {
|
||||
val userSMPServers = chatModel.userSMPServers.value
|
||||
if (userSMPServers != null) {
|
||||
if (userSMPServers.isNotEmpty()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.use_simplex_chat_servers__question),
|
||||
text = generalGetString(R.string.saved_SMP_servers_will_be_removed),
|
||||
confirmText = generalGetString(R.string.confirm_verb),
|
||||
onConfirm = {
|
||||
saveSMPServers(listOf())
|
||||
isUserSMPServers = false
|
||||
userSMPServersStr.value = ""
|
||||
}
|
||||
)
|
||||
} else {
|
||||
isUserSMPServers = false
|
||||
userSMPServersStr.value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelEdit = {
|
||||
val userSMPServers = chatModel.userSMPServers.value
|
||||
if (userSMPServers != null) {
|
||||
isUserSMPServers = userSMPServers.isNotEmpty()
|
||||
editSMPServers = !isUserSMPServers
|
||||
userSMPServersStr.value = if (isUserSMPServers) userSMPServers.joinToString(separator = "\n") else ""
|
||||
}
|
||||
},
|
||||
saveSMPServers = { saveSMPServers(it) },
|
||||
editOn = { editSMPServers = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SMPServersLayout(
|
||||
isUserSMPServers: Boolean,
|
||||
editSMPServers: Boolean,
|
||||
userSMPServersStr: MutableState<String>,
|
||||
isUserSMPServersOnOff: (Boolean) -> Unit,
|
||||
cancelEdit: () -> Unit,
|
||||
saveSMPServers: (List<String>) -> Unit,
|
||||
editOn: () -> Unit,
|
||||
) {
|
||||
Column {
|
||||
AppBarTitle(stringResource(R.string.your_SMP_servers))
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
SectionItemViewSpaceBetween(padding = PaddingValues()) {
|
||||
Text(stringResource(R.string.configure_SMP_servers), Modifier.padding(end = 24.dp))
|
||||
Switch(
|
||||
checked = isUserSMPServers,
|
||||
onCheckedChange = isUserSMPServersOnOff,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!isUserSMPServers) {
|
||||
Text(stringResource(R.string.using_simplex_chat_servers), lineHeight = 22.sp)
|
||||
} else {
|
||||
Text(stringResource(R.string.enter_one_SMP_server_per_line))
|
||||
if (editSMPServers) {
|
||||
TextEditor(Modifier.height(160.dp), text = userSMPServersStr)
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.cancel_verb),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = cancelEdit)
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
Text(
|
||||
stringResource(R.string.save_servers_button),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
val servers = userSMPServersStr.value.split("\n")
|
||||
saveSMPServers(servers)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
howToButton()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.height(160.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||
) {
|
||||
SelectionContainer(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
userSMPServersStr.value,
|
||||
Modifier
|
||||
.padding(vertical = 5.dp, horizontal = 7.dp),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 14.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text(
|
||||
stringResource(R.string.edit_verb),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = editOn)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
howToButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun howToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { uriHandler.openUri("https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent") }
|
||||
) {
|
||||
Text(stringResource(R.string.how_to), color = MaterialTheme.colors.primary)
|
||||
Icon(
|
||||
Icons.Outlined.OpenInNew, stringResource(R.string.how_to), tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(horizontal = 5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewSMPServersLayoutDefaultServers() {
|
||||
SimpleXTheme {
|
||||
SMPServersLayout(
|
||||
isUserSMPServers = false,
|
||||
editSMPServers = true,
|
||||
userSMPServersStr = remember { mutableStateOf("") },
|
||||
isUserSMPServersOnOff = {},
|
||||
cancelEdit = {},
|
||||
saveSMPServers = {},
|
||||
editOn = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewSMPServersLayoutUserServersEditOn() {
|
||||
SimpleXTheme {
|
||||
SMPServersLayout(
|
||||
isUserSMPServers = true,
|
||||
editSMPServers = true,
|
||||
userSMPServersStr = remember { mutableStateOf("smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im\nsmp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im") },
|
||||
isUserSMPServersOnOff = {},
|
||||
cancelEdit = {},
|
||||
saveSMPServers = {},
|
||||
editOn = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewSMPServersLayoutUserServersEditOff() {
|
||||
SimpleXTheme {
|
||||
SMPServersLayout(
|
||||
isUserSMPServers = true,
|
||||
editSMPServers = false,
|
||||
userSMPServersStr = remember { mutableStateOf("smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im\nsmp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im") },
|
||||
isUserSMPServersOnOff = {},
|
||||
cancelEdit = {},
|
||||
saveSMPServers = {},
|
||||
editOn = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SMPServersView(m: ChatModel) {
|
||||
var servers by remember {
|
||||
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
|
||||
}
|
||||
val testing = rememberSaveable { mutableStateOf(false) }
|
||||
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
|
||||
val saveDisabled = remember {
|
||||
derivedStateOf {
|
||||
servers.isEmpty() || servers == m.userSMPServers.value || testing.value || !servers.all { srv ->
|
||||
val address = parseServerAddress(srv.server)
|
||||
address != null && uniqueAddress(srv, address, servers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showServer(server: ServerCfg) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
var old by remember { mutableStateOf(server) }
|
||||
val index = servers.indexOf(old)
|
||||
SMPServerView(
|
||||
m,
|
||||
old,
|
||||
onUpdate = { updated ->
|
||||
val newServers = ArrayList(servers)
|
||||
newServers.removeAt(index)
|
||||
newServers.add(index, updated)
|
||||
old = updated
|
||||
servers = newServers
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
},
|
||||
onDelete = {
|
||||
val newServers = ArrayList(servers)
|
||||
newServers.removeAt(index)
|
||||
servers = newServers
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
close()
|
||||
})
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SMPServersLayout(
|
||||
testing.value,
|
||||
servers,
|
||||
serversUnchanged.value,
|
||||
saveDisabled.value,
|
||||
addServer = {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(R.string.smp_servers_add),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = servers + ServerCfg.empty
|
||||
// No saving until something will be changed on the next screen to prevent blank servers on the list
|
||||
showServer(servers.last())
|
||||
}) {
|
||||
Text(stringResource(R.string.smp_servers_enter_manually))
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ScanSMPServer {
|
||||
close()
|
||||
servers = servers + it
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.smp_servers_scan_qr))
|
||||
}
|
||||
val hasAllPresets = hasAllPresets(servers, m)
|
||||
if (!hasAllPresets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
|
||||
}) {
|
||||
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
testServers = {
|
||||
scope.launch {
|
||||
testServers(testing, servers, m) {
|
||||
servers = it
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
}
|
||||
}
|
||||
},
|
||||
resetServers = {
|
||||
servers = m.userSMPServers.value ?: emptyList()
|
||||
m.userSMPServersUnsaved.value = null
|
||||
},
|
||||
saveSMPServers = {
|
||||
saveSMPServers(servers, m)
|
||||
},
|
||||
showServer = ::showServer,
|
||||
)
|
||||
|
||||
if (testing.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServersLayout(
|
||||
testing: Boolean,
|
||||
servers: List<ServerCfg>,
|
||||
serversUnchanged: Boolean,
|
||||
saveDisabled: Boolean,
|
||||
addServer: () -> Unit,
|
||||
testServers: () -> Unit,
|
||||
resetServers: () -> Unit,
|
||||
saveSMPServers: () -> Unit,
|
||||
showServer: (ServerCfg) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_SMP_servers))
|
||||
|
||||
SectionView(stringResource(R.string.smp_servers).uppercase()) {
|
||||
for (srv in servers) {
|
||||
SectionItemView({ showServer(srv) }, disabled = testing) {
|
||||
SmpServerView(srv, servers, testing)
|
||||
}
|
||||
SectionDivider()
|
||||
}
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Add,
|
||||
stringResource(R.string.smp_servers_add),
|
||||
addServer,
|
||||
disabled = testing,
|
||||
textColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary,
|
||||
iconColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
SectionItemView(resetServers, disabled = serversUnchanged) {
|
||||
Text(stringResource(R.string.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView(testServers, disabled = testing) {
|
||||
Text(stringResource(R.string.smp_servers_test_servers), color = if (!testing) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView(saveSMPServers, disabled = saveDisabled) {
|
||||
Text(stringResource(R.string.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
HowToButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
|
||||
val address = parseServerAddress(srv.server)
|
||||
when {
|
||||
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
|
||||
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
|
||||
else -> ShowTestStatus(srv)
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
val text = address?.hostnames?.firstOrNull() ?: srv.server
|
||||
if (srv.enabled) {
|
||||
Text(text, color = if (disabled) HighOrLowlight else MaterialTheme.colors.onBackground, maxLines = 1)
|
||||
} else {
|
||||
Text(text, maxLines = 1, color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.OpenInNew,
|
||||
stringResource(R.string.how_to_use_your_servers),
|
||||
{ uriHandler.openUri("https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent") },
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InvalidServer() {
|
||||
Icon(Icons.Outlined.ErrorOutline, null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
|
||||
private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<ServerCfg>): Boolean = servers.all { srv ->
|
||||
address.hostnames.all { host ->
|
||||
srv.id == s.id || !srv.server.contains(host)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
|
||||
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
|
||||
|
||||
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
val toAdd = ArrayList<ServerCfg>()
|
||||
for (srv in m.presetSMPServers.value ?: emptyList()) {
|
||||
if (!hasPreset(srv, servers)) {
|
||||
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
|
||||
}
|
||||
}
|
||||
return toAdd
|
||||
}
|
||||
|
||||
private fun hasPreset(srv: String, servers: List<ServerCfg>): Boolean =
|
||||
servers.any { it.server == srv }
|
||||
|
||||
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit) {
|
||||
val resetStatus = resetTestStatus(servers)
|
||||
onUpdated(resetStatus)
|
||||
testing.value = true
|
||||
val fs = runServersTest(resetStatus, m) { onUpdated(it) }
|
||||
testing.value = false
|
||||
if (fs.isNotEmpty()) {
|
||||
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.smp_servers_test_failed),
|
||||
text = generalGetString(R.string.smp_servers_test_some_failed) + "\n" + msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
|
||||
val copy = ArrayList(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
copy.removeAt(index)
|
||||
copy.add(index, server.copy(tested = null))
|
||||
}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
|
||||
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
|
||||
val updatedServers = ArrayList<ServerCfg>(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
val (updatedServer, f) = testServerConnection(server, m)
|
||||
updatedServers.removeAt(index)
|
||||
updatedServers.add(index, updatedServer)
|
||||
// toList() is important. Otherwise, Compose will not redraw the screen after first update
|
||||
onUpdated(updatedServers.toList())
|
||||
if (f != null) {
|
||||
fs[serverHostname(updatedServer)] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel) {
|
||||
withApi {
|
||||
if (m.controller.setUserSMPServers(servers)) {
|
||||
m.userSMPServers.value = servers
|
||||
m.userSMPServersUnsaved.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.app.model.ServerCfg
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCodeScanner
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanSMPServerLayout(onNext)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.smp_servers_scan_qr), false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = 12.dp)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
if (res != null) {
|
||||
onNext(ServerCfg(text, false, null, true))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.smp_servers_invalid_address),
|
||||
text = generalGetString(R.string.smp_servers_check_address)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,6 +394,26 @@ fun SettingsPreferenceItemWithInfo(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceToggle(
|
||||
text: String,
|
||||
checked: Boolean,
|
||||
onChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceToggleWithIcon(
|
||||
text: String,
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontaktanfrage</string>
|
||||
<string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string>
|
||||
<string name="error_changing_address">Fehler beim Wechseln der Adresse</string>
|
||||
<string name="error_smp_test_failed_at_step">***Test failed at step %s.</string>
|
||||
<string name="error_smp_test_server_auth">***Server requires authorization to create queues, check password</string>
|
||||
<string name="error_smp_test_certificate">***Possibly, certificate fingerprint in server address is incorrect</string>
|
||||
<string name="smp_server_test_connect">***Connect</string>
|
||||
<string name="smp_server_test_create_queue">***Create queue</string>
|
||||
<string name="smp_server_test_secure_queue">***Secure queue</string>
|
||||
<string name="smp_server_test_delete_queue">***Delete queue</string>
|
||||
<string name="smp_server_test_disconnect">***Disconnect</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string>
|
||||
@@ -235,6 +243,7 @@
|
||||
<string name="back">Zurück</string>
|
||||
<string name="cancel_verb">Abbrechen</string>
|
||||
<string name="confirm_verb">Bestätigen</string>
|
||||
<string name="reset_verb">***Reset</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">Keine Details</string>
|
||||
<string name="add_contact">Kontakt hinzufügen</string>
|
||||
@@ -368,17 +377,34 @@
|
||||
<string name="chat_lock">SimpleX Sperre</string>
|
||||
<string name="chat_console">Chat Konsole</string>
|
||||
<string name="smp_servers">SMP-Server</string>
|
||||
<string name="smp_servers_preset_address">***Preset server address</string>
|
||||
<string name="smp_servers_preset_add">***Add preset servers</string>
|
||||
<string name="smp_servers_add">***Add server…</string>
|
||||
<string name="smp_servers_test_server">***Test server</string>
|
||||
<string name="smp_servers_test_servers">***Test servers</string>
|
||||
<string name="smp_servers_save">***Save servers</string>
|
||||
<string name="smp_servers_test_failed">***Server test failed!</string>
|
||||
<string name="smp_servers_test_some_failed">***Some servers failed the test:</string>
|
||||
<string name="smp_servers_scan_qr">***Scan server QR code</string>
|
||||
<string name="smp_servers_enter_manually">***Enter server manually</string>
|
||||
<string name="smp_servers_preset_server">***Preset server</string>
|
||||
<string name="smp_servers_your_server">***Your server</string>
|
||||
<string name="smp_servers_your_server_address">***Your server address</string>
|
||||
<string name="smp_servers_use_server">***Use server</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">***Use for new connections</string>
|
||||
<string name="smp_servers_add_to_another_device">***Add to another device</string>
|
||||
<string name="smp_servers_invalid_address">***Invalid server address!</string>
|
||||
<string name="smp_servers_check_address">***Check server address and try again.</string>
|
||||
<string name="smp_servers_delete_server">***Delete server</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installieren Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> als Terminalanwendung</string>
|
||||
<string name="star_on_github">Stern auf GitHub vergeben</string>
|
||||
<string name="contribute">Unterstützen Sie uns</string>
|
||||
<string name="rate_the_app">Bewerten Sie die App</string>
|
||||
<string name="use_simplex_chat_servers__question">Verwenden Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Server?</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Gespeicherte SMP-Server werden entfernt.</string>
|
||||
<string name="your_SMP_servers">Ihre SMP-Server</string>
|
||||
<string name="configure_SMP_servers">SMP-Server konfigurieren</string>
|
||||
<string name="using_simplex_chat_servers">Verwendung von <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Servern.</string>
|
||||
<string name="enter_one_SMP_server_per_line">SMP-Server (einer pro Zeile)</string>
|
||||
<string name="how_to">Anleitung</string>
|
||||
<string name="how_to_use_your_servers">***How to use your servers</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC ICE-Server werden entfernt.</string>
|
||||
<string name="your_ICE_servers">Ihre ICE-Server</string>
|
||||
<string name="configure_ICE_servers">Konfigurieren Sie ICE-Server</string>
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
<string name="error_deleting_contact_request">Ошибка удаления запроса</string>
|
||||
<string name="error_deleting_pending_contact_connection">Ошибка удаления ожидаемого соединения</string>
|
||||
<string name="error_changing_address">Ошибка при изменении адреса</string>
|
||||
<string name="error_smp_test_failed_at_step">Ошибка теста на шаге %s.</string>
|
||||
<string name="error_smp_test_server_auth">Сервер требует авторизации для создания очередей, проверьте пароль</string>
|
||||
<string name="error_smp_test_certificate">Возможно, хэш сертификата в адресе сервера неверный</string>
|
||||
<string name="smp_server_test_connect">Соединение</string>
|
||||
<string name="smp_server_test_create_queue">Создание очереди</string>
|
||||
<string name="smp_server_test_secure_queue">Защита очереди</string>
|
||||
<string name="smp_server_test_delete_queue">Удаление очереди</string>
|
||||
<string name="smp_server_test_disconnect">Разрыв соединения</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Мгновенные уведомления</string>
|
||||
@@ -235,6 +243,7 @@
|
||||
<string name="back">Назад</string>
|
||||
<string name="cancel_verb">Отменить</string>
|
||||
<string name="confirm_verb">Подтвердить</string>
|
||||
<string name="reset_verb">Сбросить</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">нет описания</string>
|
||||
<string name="add_contact">Одноразовая ссылка</string>
|
||||
@@ -365,17 +374,34 @@
|
||||
<string name="chat_lock">Блокировка SimpleX</string>
|
||||
<string name="chat_console">Консоль</string>
|
||||
<string name="smp_servers">SMP серверы</string>
|
||||
<string name="smp_servers_preset_address">Адрес сервера по умолчанию</string>
|
||||
<string name="smp_servers_preset_add">Добавить серверы по умолчанию</string>
|
||||
<string name="smp_servers_add">Добавить сервер…</string>
|
||||
<string name="smp_servers_test_server">Тестировать сервер</string>
|
||||
<string name="smp_servers_test_servers">Тестировать серверы</string>
|
||||
<string name="smp_servers_save">Сохранить серверы</string>
|
||||
<string name="smp_servers_test_failed">Ошибка теста сервера!</string>
|
||||
<string name="smp_servers_test_some_failed">Серверы не прошли тест:</string>
|
||||
<string name="smp_servers_scan_qr">Сканировать QR код сервера</string>
|
||||
<string name="smp_servers_enter_manually">Ввести сервер вручную</string>
|
||||
<string name="smp_servers_preset_server">Сервер по умолчанию</string>
|
||||
<string name="smp_servers_your_server">Ваш сервер</string>
|
||||
<string name="smp_servers_your_server_address">Адрес вашего сервера</string>
|
||||
<string name="smp_servers_use_server">Использовать сервер</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Использовать для новых соединений</string>
|
||||
<string name="smp_servers_add_to_another_device">Добавить на другое устройство</string>
|
||||
<string name="smp_servers_invalid_address">Ошибка в адресе сервера!</string>
|
||||
<string name="smp_servers_check_address">Проверьте адрес сервера и попробуйте снова.</string>
|
||||
<string name="smp_servers_delete_server">Удалить сервер</string>
|
||||
<string name="install_simplex_chat_for_terminal"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</string>
|
||||
<string name="star_on_github">Поставить звездочку в GitHub</string>
|
||||
<string name="contribute">Внести свой вклад</string>
|
||||
<string name="rate_the_app">Оценить приложение</string>
|
||||
<string name="use_simplex_chat_servers__question">Использовать серверы предосталенные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>?</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Сохраненные SMP серверы будут удалены.</string>
|
||||
<string name="your_SMP_servers">Ваши SMP серверы</string>
|
||||
<string name="configure_SMP_servers">Настройка SMP серверов</string>
|
||||
<string name="using_simplex_chat_servers">Используются серверы предоставленные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
|
||||
<string name="enter_one_SMP_server_per_line">Введите SMP серверы, каждый сервер в отдельной строке:</string>
|
||||
<string name="how_to">Инфо</string>
|
||||
<string name="how_to_use_your_servers">Как использовать серверы</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Сохраненные WebRTC ICE серверы будут удалены.</string>
|
||||
<string name="your_ICE_servers">Ваши ICE серверы</string>
|
||||
<string name="configure_ICE_servers">Настройка ICE серверов</string>
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
<string name="error_deleting_contact_request">Error deleting contact request</string>
|
||||
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
|
||||
<string name="error_changing_address">Error changing address</string>
|
||||
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
|
||||
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
|
||||
<string name="error_smp_test_certificate">Possibly, certificate fingerprint in server address is incorrect</string>
|
||||
<string name="smp_server_test_connect">Connect</string>
|
||||
<string name="smp_server_test_create_queue">Create queue</string>
|
||||
<string name="smp_server_test_secure_queue">Secure queue</string>
|
||||
<string name="smp_server_test_delete_queue">Delete queue</string>
|
||||
<string name="smp_server_test_disconnect">Disconnect</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
@@ -235,6 +243,7 @@
|
||||
<string name="back">Back</string>
|
||||
<string name="cancel_verb">Cancel</string>
|
||||
<string name="confirm_verb">Confirm</string>
|
||||
<string name="reset_verb">Reset</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">no details</string>
|
||||
<string name="add_contact">One-time invitation link</string>
|
||||
@@ -368,17 +377,34 @@
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="smp_servers_preset_address">Preset server address</string>
|
||||
<string name="smp_servers_preset_add">Add preset servers</string>
|
||||
<string name="smp_servers_add">Add server…</string>
|
||||
<string name="smp_servers_test_server">Test server</string>
|
||||
<string name="smp_servers_test_servers">Test servers</string>
|
||||
<string name="smp_servers_save">Save servers</string>
|
||||
<string name="smp_servers_test_failed">Server test failed!</string>
|
||||
<string name="smp_servers_test_some_failed">Some servers failed the test:</string>
|
||||
<string name="smp_servers_scan_qr">Scan server QR code</string>
|
||||
<string name="smp_servers_enter_manually">Enter server manually</string>
|
||||
<string name="smp_servers_preset_server">Preset server</string>
|
||||
<string name="smp_servers_your_server">Your server</string>
|
||||
<string name="smp_servers_your_server_address">Your server address</string>
|
||||
<string name="smp_servers_use_server">Use server</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Use for new connections</string>
|
||||
<string name="smp_servers_add_to_another_device">Add to another device</string>
|
||||
<string name="smp_servers_invalid_address">Invalid server address!</string>
|
||||
<string name="smp_servers_check_address">Check server address and try again.</string>
|
||||
<string name="smp_servers_delete_server">Delete server</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
|
||||
<string name="star_on_github">Star on GitHub</string>
|
||||
<string name="contribute">Contribute</string>
|
||||
<string name="rate_the_app">Rate the app</string>
|
||||
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Saved SMP servers will be removed.</string>
|
||||
<string name="your_SMP_servers">Your SMP servers</string>
|
||||
<string name="configure_SMP_servers">Configure SMP servers</string>
|
||||
<string name="using_simplex_chat_servers">Using <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers.</string>
|
||||
<string name="enter_one_SMP_server_per_line">SMP servers (one per line)</string>
|
||||
<string name="how_to">How to</string>
|
||||
<string name="how_to_use_your_servers">How to use your servers</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Saved WebRTC ICE servers will be removed.</string>
|
||||
<string name="your_ICE_servers">Your ICE servers</string>
|
||||
<string name="configure_ICE_servers">Configure ICE servers</string>
|
||||
|
||||
Reference in New Issue
Block a user