android: servers UI/API (#2155)

* android: servers UI/API

* non-optional server protocol in parsed address

* make another enum for ServerProtocol

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2023-04-07 20:19:44 +03:00 committed by GitHub
parent 85537a99e8
commit 991332a809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 204 additions and 98 deletions

View File

@ -54,10 +54,8 @@ class ChatModel(val controller: ChatController) {
val terminalItems = mutableStateListOf<TerminalItem>()
val userAddress = mutableStateOf<UserContactLinkRec?>(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

View File

@ -337,9 +337,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun getUserChatData() {
chatModel.userAddress.value = apiGetUserAddress()
val smpServers = getUserSMPServers()
chatModel.userSMPServers.value = smpServers?.first
chatModel.presetSMPServers.value = smpServers?.second
chatModel.chatItemTTL.value = getChatItemTTL()
val chats = apiGetChats()
chatModel.updateChats(chats)
@ -579,38 +576,44 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
private suspend fun getUserSMPServers(): Pair<List<ServerCfg>, List<String>>? {
val userId = kotlin.runCatching { currentUserId("getUserSMPServers") }.getOrElse { return null }
val r = sendCmd(CC.APIGetUserSMPServers(userId))
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 getUserProtoServers(serverProtocol: ServerProtocol): UserProtocolServers? {
val userId = kotlin.runCatching { currentUserId("getUserProtoServers") }.getOrElse { return null }
val r = sendCmd(CC.APIGetUserProtoServers(userId, serverProtocol))
return if (r is CR.UserProtoServers) r.servers
else {
Log.e(TAG, "getUserProtoServers bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.error_loading_smp_servers else R.string.error_loading_xftp_servers),
"${r.responseType}: ${r.details}"
)
null
}
}
suspend fun setUserSMPServers(smpServers: List<ServerCfg>): Boolean {
val userId = kotlin.runCatching { currentUserId("setUserSMPServers") }.getOrElse { return false }
val r = sendCmd(CC.APISetUserSMPServers(userId, smpServers))
suspend fun setUserProtoServers(serverProtocol: ServerProtocol, servers: List<ServerCfg>): Boolean {
val userId = kotlin.runCatching { currentUserId("setUserProtoServers") }.getOrElse { return false }
val r = sendCmd(CC.APISetUserProtoServers(userId, serverProtocol, servers))
return when (r) {
is CR.CmdOk -> true
else -> {
Log.e(TAG, "setUserSMPServers bad response: ${r.responseType} ${r.details}")
Log.e(TAG, "setUserProtoServers bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
generalGetString(R.string.error_saving_smp_servers),
generalGetString(R.string.ensure_smp_server_address_are_correct_format_and_unique)
generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.error_saving_smp_servers else R.string.error_saving_xftp_servers),
generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.ensure_smp_server_address_are_correct_format_and_unique else R.string.ensure_xftp_server_address_are_correct_format_and_unique)
)
false
}
}
}
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
val userId = currentUserId("testSMPServer")
val r = sendCmd(CC.APITestSMPServer(userId, smpServer))
suspend fun testProtoServer(server: String): ProtocolTestFailure? {
val userId = currentUserId("testProtoServer")
val r = sendCmd(CC.APITestProtoServer(userId, server))
return when (r) {
is CR.SmpTestResult -> r.smpTestFailure
is CR.ServerTestResult -> r.testFailure
else -> {
Log.e(TAG, "testSMPServer bad response: ${r.responseType} ${r.details}")
throw Exception("testSMPServer bad response: ${r.responseType} ${r.details}")
Log.e(TAG, "testProtoServer bad response: ${r.responseType} ${r.details}")
throw Exception("testProtoServer bad response: ${r.responseType} ${r.details}")
}
}
}
@ -1865,9 +1868,9 @@ sealed class CC {
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC()
class APIGetUserSMPServers(val userId: Long): CC()
class APISetUserSMPServers(val userId: Long, val smpServers: List<ServerCfg>): CC()
class APITestSMPServer(val userId: Long, val smpServer: String): CC()
class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC()
class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC()
class APITestProtoServer(val userId: Long, val server: String): CC()
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
class APIGetChatItemTTL(val userId: Long): CC()
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
@ -1949,9 +1952,9 @@ sealed class CC {
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId"
is APIGetUserSMPServers -> "/_smp $userId"
is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}"
is APITestSMPServer -> "/_smp test $userId $smpServer"
is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}"
is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}"
is APITestProtoServer -> "/_server test $userId $server"
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
@ -2034,9 +2037,9 @@ sealed class CC {
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink"
is APIGetUserSMPServers -> "apiGetUserSMPServers"
is APISetUserSMPServers -> "apiSetUserSMPServers"
is APITestSMPServer -> "testSMPServer"
is APIGetUserProtoServers -> "apiGetUserProtoServers"
is APISetUserProtoServers -> "apiSetUserProtoServers"
is APITestProtoServer -> "testProtoServer"
is APISetChatItemTTL -> "apiSetChatItemTTL"
is APIGetChatItemTTL -> "apiGetChatItemTTL"
is APISetNetworkConfig -> "/apiSetNetworkConfig"
@ -2113,7 +2116,7 @@ sealed class CC {
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
fun smpServersStr(smpServers: List<ServerCfg>) = if (smpServers.isEmpty()) "default" else json.encodeToString(SMPServersConfig(smpServers))
fun protoServersStr(servers: List<ServerCfg>) = json.encodeToString(ProtoServersConfig(servers))
}
}
@ -2148,8 +2151,21 @@ class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? =
class DBEncryptionConfig(val currentKey: String, val newKey: String)
@Serializable
data class SMPServersConfig(
val smpServers: List<ServerCfg>
enum class ServerProtocol {
@SerialName("smp") SMP,
@SerialName("xftp") XFTP;
}
@Serializable
data class ProtoServersConfig(
val servers: List<ServerCfg>
)
@Serializable
data class UserProtocolServers(
val serverProtocol: ServerProtocol,
val protoServers: List<ServerCfg>,
val presetServers: List<String>,
)
@Serializable
@ -2203,29 +2219,39 @@ data class ServerCfg(
}
@Serializable
enum class SMPTestStep {
enum class ProtocolTestStep {
@SerialName("connect") Connect,
@SerialName("disconnect") Disconnect,
@SerialName("createQueue") CreateQueue,
@SerialName("secureQueue") SecureQueue,
@SerialName("deleteQueue") DeleteQueue,
@SerialName("disconnect") Disconnect;
@SerialName("createFile") CreateFile,
@SerialName("uploadFile") UploadFile,
@SerialName("downloadFile") DownloadFile,
@SerialName("compareFile") CompareFile,
@SerialName("deleteFile") DeleteFile;
val text: String get() = when (this) {
Connect -> generalGetString(R.string.smp_server_test_connect)
Disconnect -> generalGetString(R.string.smp_server_test_disconnect)
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)
CreateFile -> generalGetString(R.string.smp_server_test_create_file)
UploadFile -> generalGetString(R.string.smp_server_test_upload_file)
DownloadFile -> generalGetString(R.string.smp_server_test_download_file)
CompareFile -> generalGetString(R.string.smp_server_test_compare_file)
DeleteFile -> generalGetString(R.string.smp_server_test_delete_file)
}
}
@Serializable
data class SMPTestFailure(
val testStep: SMPTestStep,
data class ProtocolTestFailure(
val testStep: ProtocolTestStep,
val testError: AgentErrorType
) {
override fun equals(other: Any?): Boolean {
if (other !is SMPTestFailure) return false
if (other !is ProtocolTestFailure) return false
return other.testStep == this.testStep
}
@ -2238,6 +2264,8 @@ data class SMPTestFailure(
return when {
testError is AgentErrorType.SMP && testError.smpErr is SMPErrorType.AUTH ->
err + " " + generalGetString(R.string.error_smp_test_server_auth)
testError is AgentErrorType.XFTP && testError.xftpErr is XFTPErrorType.AUTH ->
err + " " + generalGetString(R.string.error_xftp_test_server_auth)
testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK ->
err + " " + generalGetString(R.string.error_smp_test_certificate)
else -> err
@ -2247,6 +2275,7 @@ data class SMPTestFailure(
@Serializable
data class ServerAddress(
val serverProtocol: ServerProtocol,
val hostnames: List<String>,
val port: String,
val keyHash: String,
@ -2254,19 +2283,21 @@ data class ServerAddress(
) {
val uri: String
get() =
"smp://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}"
"${serverProtocol}://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}"
val valid: Boolean
get() = hostnames.isNotEmpty() && hostnames.toSet().size == hostnames.size
companion object {
val empty = ServerAddress(
fun empty(serverProtocol: ServerProtocol) = ServerAddress(
serverProtocol = serverProtocol,
hostnames = emptyList(),
port = "",
keyHash = "",
basicAuth = ""
)
val sampleData = ServerAddress(
serverProtocol = ServerProtocol.SMP,
hostnames = listOf("smp.simplex.im", "1234.onion"),
port = "",
keyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=",
@ -3011,8 +3042,8 @@ sealed class CR {
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
@Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List<Chat>): CR()
@Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR()
@Serializable @SerialName("userSMPServers") class UserSMPServers(val user: User, val smpServers: List<ServerCfg>, val presetSMPServers: List<String>): CR()
@Serializable @SerialName("smpTestResult") class SmpTestResult(val user: User, val smpTestFailure: SMPTestFailure? = null): CR()
@Serializable @SerialName("userProtoServers") class UserProtoServers(val user: User, val servers: UserProtocolServers): CR()
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: User, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR()
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
@Serializable @SerialName("contactInfo") class ContactInfo(val user: User, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR()
@ -3119,8 +3150,8 @@ sealed class CR {
is ChatStopped -> "chatStopped"
is ApiChats -> "apiChats"
is ApiChat -> "apiChat"
is UserSMPServers -> "userSMPServers"
is SmpTestResult -> "smpTestResult"
is UserProtoServers -> "userProtoServers"
is ServerTestResult -> "serverTestResult"
is ChatItemTTL -> "chatItemTTL"
is NetworkConfig -> "networkConfig"
is ContactInfo -> "contactInfo"
@ -3225,8 +3256,8 @@ sealed class CR {
is ChatStopped -> noDetails()
is ApiChats -> withUser(user, json.encodeToString(chats))
is ApiChat -> withUser(user, json.encodeToString(chat))
is UserSMPServers -> withUser(user, "$smpServers: ${json.encodeToString(smpServers)}\n$presetSMPServers: ${json.encodeToString(presetSMPServers)}")
is SmpTestResult -> withUser(user, json.encodeToString(smpTestFailure))
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
is NetworkConfig -> json.encodeToString(networkConfig)
is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
@ -3459,6 +3490,7 @@ sealed class AgentErrorType {
is CMD -> "CMD ${cmdErr.string}"
is CONN -> "CONN ${connErr.string}"
is SMP -> "SMP ${smpErr.string}"
is XFTP -> "XFTP ${xftpErr.string}"
is BROKER -> "BROKER ${brokerErr.string}"
is AGENT -> "AGENT ${agentErr.string}"
is INTERNAL -> "INTERNAL $internalErr"
@ -3466,6 +3498,7 @@ sealed class AgentErrorType {
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
@Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()
@Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType()
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
@ -3533,7 +3566,7 @@ sealed class SMPErrorType {
}
@Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType()
@Serializable @SerialName("SESSION") class SESSION: SMPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: SMPCommandError): SMPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType()
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
@Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType()
@ -3542,7 +3575,7 @@ sealed class SMPErrorType {
}
@Serializable
sealed class SMPCommandError {
sealed class ProtocolCommandError {
val string: String get() = when (this) {
is UNKNOWN -> "UNKNOWN"
is SYNTAX -> "SYNTAX"
@ -3550,11 +3583,11 @@ sealed class SMPCommandError {
is HAS_AUTH -> "HAS_AUTH"
is NO_QUEUE -> "NO_QUEUE"
}
@Serializable @SerialName("UNKNOWN") class UNKNOWN: SMPCommandError()
@Serializable @SerialName("SYNTAX") class SYNTAX: SMPCommandError()
@Serializable @SerialName("NO_AUTH") class NO_AUTH: SMPCommandError()
@Serializable @SerialName("HAS_AUTH") class HAS_AUTH: SMPCommandError()
@Serializable @SerialName("NO_QUEUE") class NO_QUEUE: SMPCommandError()
@Serializable @SerialName("UNKNOWN") class UNKNOWN: ProtocolCommandError()
@Serializable @SerialName("SYNTAX") class SYNTAX: ProtocolCommandError()
@Serializable @SerialName("NO_AUTH") class NO_AUTH: ProtocolCommandError()
@Serializable @SerialName("HAS_AUTH") class HAS_AUTH: ProtocolCommandError()
@Serializable @SerialName("NO_QUEUE") class NO_QUEUE: ProtocolCommandError()
}
@Serializable
@ -3596,3 +3629,33 @@ sealed class SMPAgentError {
@Serializable @SerialName("A_VERSION") class A_VERSION: SMPAgentError()
@Serializable @SerialName("A_ENCRYPTION") class A_ENCRYPTION: SMPAgentError()
}
@Serializable
sealed class XFTPErrorType {
val string: String get() = when (this) {
is BLOCK -> "BLOCK"
is SESSION -> "SESSION"
is CMD -> "CMD ${cmdErr.string}"
is AUTH -> "AUTH"
is SIZE -> "SIZE"
is QUOTA -> "QUOTA"
is DIGEST -> "DIGEST"
is CRYPTO -> "CRYPTO"
is NO_FILE -> "NO_FILE"
is HAS_FILE -> "HAS_FILE"
is FILE_IO -> "FILE_IO"
is INTERNAL -> "INTERNAL"
}
@Serializable @SerialName("BLOCK") object BLOCK: XFTPErrorType()
@Serializable @SerialName("SESSION") object SESSION: XFTPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): XFTPErrorType()
@Serializable @SerialName("AUTH") object AUTH: XFTPErrorType()
@Serializable @SerialName("SIZE") object SIZE: XFTPErrorType()
@Serializable @SerialName("QUOTA") object QUOTA: XFTPErrorType()
@Serializable @SerialName("DIGEST") object DIGEST: XFTPErrorType()
@Serializable @SerialName("CRYPTO") object CRYPTO: XFTPErrorType()
@Serializable @SerialName("NO_FILE") object NO_FILE: XFTPErrorType()
@Serializable @SerialName("HAS_FILE") object HAS_FILE: XFTPErrorType()
@Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType()
@Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType()
}

View File

@ -40,6 +40,7 @@ fun NetworkAndServersView(
NetworkAndServersLayout(
developerTools = developerTools,
xftpSendEnabled = remember { chatModel.controller.appPrefs.xftpSendEnabled.state },
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
@ -135,6 +136,7 @@ fun NetworkAndServersView(
@Composable fun NetworkAndServersLayout(
developerTools: Boolean,
xftpSendEnabled: State<Boolean>,
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
@ -152,8 +154,14 @@ 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), showCustomModal { m, close -> SMPServersView(m, close) })
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) })
SectionDivider()
if (xftpSendEnabled.value) {
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) })
SectionDivider()
}
SectionItemView {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
}
@ -297,6 +305,7 @@ fun PreviewNetworkAndServersLayout() {
SimpleXTheme {
NetworkAndServersLayout(
developerTools = true,
xftpSendEnabled = remember { mutableStateOf(true) },
networkUseSocksProxy = remember { mutableStateOf(true) },
showModal = { {} },
showSettingsModal = { {} },

View File

@ -32,12 +32,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable
fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
var testing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
SMPServerLayout(
ProtocolServerLayout(
testing,
server,
serverProtocol,
testServer = {
testing = true
scope.launch {
@ -68,9 +69,10 @@ fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit
}
@Composable
private fun SMPServerLayout(
private fun ProtocolServerLayout(
testing: Boolean,
server: ServerCfg,
serverProtocol: ServerProtocol,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
@ -86,7 +88,7 @@ private fun SMPServerLayout(
if (server.preset) {
PresetServer(testing, server, testServer, onUpdate, onDelete)
} else {
CustomServer(testing, server, testServer, onUpdate, onDelete)
CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete)
}
}
}
@ -119,12 +121,19 @@ private fun PresetServer(
private fun CustomServer(
testing: Boolean,
server: ServerCfg,
serverProtocol: ServerProtocol,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
val serverAddress = remember { mutableStateOf(server.server) }
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
val valid = remember {
derivedStateOf {
with(parseServerAddress(serverAddress.value)) {
this?.valid == true && this.serverProtocol == serverProtocol
}
}
}
SectionView(
stringResource(R.string.smp_servers_your_server_address).uppercase(),
icon = Icons.Outlined.ErrorOutline,
@ -187,9 +196,9 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent)
}
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, SMPTestFailure?> =
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
try {
val r = m.controller.testSMPServer(server.server)
val r = m.controller.testProtoServer(server.server)
server.copy(tested = r == null) to r
} catch (e: Exception) {
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")

View File

@ -27,17 +27,19 @@ import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.launch
@Composable
fun SMPServersView(m: ChatModel, close: () -> Unit) {
fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember { mutableStateOf(emptyList<String>()) }
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
}
val currServers = remember { mutableStateOf(servers) }
val testing = rememberSaveable { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
derivedStateOf {
servers.isEmpty() ||
servers == m.userSMPServers.value ||
servers == currServers.value ||
testing.value ||
!servers.all { srv ->
val address = parseServerAddress(srv.server)
@ -47,13 +49,25 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
}
LaunchedEffect(Unit) {
val res = m.controller.getUserProtoServers(serverProtocol)
if (res != null) {
currServers.value = res.protoServers
presetServers = res.presetServers
if (servers.isEmpty()) {
servers = currServers.value
}
}
}
fun showServer(server: ServerCfg) {
ModalManager.shared.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) }
val index = servers.indexOf(old)
SMPServerView(
ProtocolServerView(
m,
old,
serverProtocol,
onUpdate = { updated ->
val newServers = ArrayList(servers)
newServers.removeAt(index)
@ -75,11 +89,12 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
ModalView(
close = {
if (saveDisabled.value) close()
else showUnsavedChangesAlert({ saveSMPServers(servers, m, close) }, close)
else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
SMPServersLayout(
ProtocolServersLayout(
serverProtocol,
testing = testing.value,
servers = servers,
serversUnchanged = serversUnchanged.value,
@ -102,7 +117,7 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ScanSMPServer {
ScanProtocolServer {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
@ -112,11 +127,11 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
) {
Text(stringResource(R.string.smp_servers_scan_qr))
}
val hasAllPresets = hasAllPresets(servers, m)
val hasAllPresets = hasAllPresets(presetServers, servers, m)
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
}
@ -134,11 +149,11 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
},
resetServers = {
servers = m.userSMPServers.value ?: emptyList()
servers = currServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveSMPServers(servers, m)
saveServers(serverProtocol, currServers, servers, m)
},
showServer = ::showServer,
)
@ -161,7 +176,8 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
@Composable
private fun SMPServersLayout(
private fun ProtocolServersLayout(
serverProtocol: ServerProtocol,
testing: Boolean,
servers: List<ServerCfg>,
serversUnchanged: Boolean,
@ -180,12 +196,12 @@ private fun SMPServersLayout(
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(R.string.your_SMP_servers))
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.your_SMP_servers else R.string.your_XFTP_servers))
SectionView(stringResource(R.string.smp_servers).uppercase()) {
SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.smp_servers else R.string.xftp_servers).uppercase()) {
for (srv in servers) {
SectionItemView({ showServer(srv) }, disabled = testing) {
SmpServerView(srv, servers, testing)
ProtocolServerView(serverProtocol, srv, servers, testing)
}
SectionDivider()
}
@ -232,10 +248,10 @@ private fun SMPServersLayout(
}
@Composable
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
val address = parseServerAddress(srv.server)
when {
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
else -> ShowTestStatus(srv)
}
@ -271,12 +287,12 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<Se
}
}
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean =
presetServers.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
private fun addAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
val toAdd = ArrayList<ServerCfg>()
for (srv in m.presetSMPServers.value ?: emptyList()) {
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
}
@ -313,8 +329,8 @@ private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
return copy
}
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, ProtocolTestFailure> {
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
@ -331,10 +347,10 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
return fs
}
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
private fun saveServers(protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
withApi {
if (m.controller.setUserSMPServers(servers)) {
m.userSMPServers.value = servers
if (m.controller.setUserProtoServers(protocol, servers)) {
currServers.value = servers
m.userSMPServersUnsaved.value = null
}
afterSave()

View File

@ -2,7 +2,6 @@ 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
@ -17,16 +16,16 @@ import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanSMPServerLayout(onNext)
ScanProtocolServerLayout(onNext)
}
@Composable
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
private fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
Column(
Modifier
.fillMaxSize()

View File

@ -60,7 +60,11 @@
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Error saving SMP servers</string>
<string name="error_saving_xftp_servers">Error saving XFTP servers</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Make sure SMP server addresses are in correct format, line separated and are not duplicated.</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Make sure XFTP server addresses are in correct format, line separated and are not duplicated.</string>
<string name="error_loading_smp_servers">Error loading SMP servers</string>
<string name="error_loading_xftp_servers">Error loading XFTP servers</string>
<string name="error_setting_network_config">Error updating network configuration</string>
<string name="failed_to_parse_chat_title">Failed to load chat</string>
<string name="failed_to_parse_chats_title">Failed to load chats</string>
@ -96,12 +100,18 @@
<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_xftp_test_server_auth">Server requires authorization to upload, 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_disconnect">Disconnect</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>
<string name="smp_server_test_create_file">Create file</string>
<string name="smp_server_test_upload_file">Upload file</string>
<string name="smp_server_test_download_file">Download file</string>
<string name="smp_server_test_compare_file">Compare file</string>
<string name="smp_server_test_delete_file">Delete file</string>
<string name="error_deleting_user">Error deleting user profile</string>
<string name="error_updating_user_privacy">Error updating user privacy</string>
@ -476,12 +486,14 @@
<string name="smp_servers_delete_server">Delete server</string>
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
<string name="smp_save_servers_question">Save servers?</string>
<string name="xftp_servers">XFTP servers</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="your_SMP_servers">Your SMP servers</string>
<string name="your_XFTP_servers">Your XFTP servers</string>
<string name="using_simplex_chat_servers">Using <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers.</string>
<string name="how_to">How to</string>
<string name="how_to_use_your_servers">How to use your servers</string>
@ -732,7 +744,7 @@
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_incognito">Incognito mode</string>
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>

View File

@ -602,7 +602,7 @@ public enum ChatResponse: Decodable, Error {
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
case let .apiChat(u, chat): return withUser(u, String(describing: chat))
case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))")
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\result: \(String(describing: testFailure))")
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
case let .networkConfig(networkConfig): return String(describing: networkConfig)
case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))")