diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 7646abe8a2..a25d56a337 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -54,10 +54,8 @@ class ChatModel(val controller: ChatController) { val terminalItems = mutableStateListOf() val userAddress = mutableStateOf(null) - val userSMPServers = mutableStateOf<(List)?>(null) // Allows to temporary save servers that are being edited on multiple screens val userSMPServersUnsaved = mutableStateOf<(List)?>(null) - val presetSMPServers = mutableStateOf<(List)?>(null) val chatItemTTL = mutableStateOf(ChatItemTTL.None) // set when app opened from external intent diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 38927e4ea3..c14eff4481 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -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>? { - 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): Boolean { - val userId = kotlin.runCatching { currentUserId("setUserSMPServers") }.getOrElse { return false } - val r = sendCmd(CC.APISetUserSMPServers(userId, smpServers)) + suspend fun setUserProtoServers(serverProtocol: ServerProtocol, servers: List): 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): 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): 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) = if (smpServers.isEmpty()) "default" else json.encodeToString(SMPServersConfig(smpServers)) + fun protoServersStr(servers: List) = 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 +enum class ServerProtocol { + @SerialName("smp") SMP, + @SerialName("xftp") XFTP; +} + +@Serializable +data class ProtoServersConfig( + val servers: List +) + +@Serializable +data class UserProtocolServers( + val serverProtocol: ServerProtocol, + val protoServers: List, + val presetServers: List, ) @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, 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): CR() @Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR() - @Serializable @SerialName("userSMPServers") class UserSMPServers(val user: User, val smpServers: List, val presetSMPServers: List): 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() +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt index 36cffb693f..1ed408f150 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt @@ -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, networkUseSocksProxy: MutableState, onionHosts: MutableState, sessionMode: MutableState, @@ -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 = { {} }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServerView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt similarity index 89% rename from apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServerView.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt index e6d61332ce..923693790b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServerView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt @@ -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 = +suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair = 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()}") diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt similarity index 79% rename from apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServersView.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt index 8286121c33..210db9f61d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt @@ -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()) } 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, 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, disabled: Boolean) { +private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List, 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, m: ChatModel): Boolean = - m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true +private fun hasAllPresets(presetServers: List, servers: List, m: ChatModel): Boolean = + presetServers.all { hasPreset(it, servers) } ?: true -private fun addAllPresets(servers: List, m: ChatModel): List { +private fun addAllPresets(presetServers: List, servers: List, m: ChatModel): List { val toAdd = ArrayList() - 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): List { return copy } -private suspend fun runServersTest(servers: List, m: ChatModel, onUpdated: (List) -> Unit): Map { - val fs: MutableMap = mutableMapOf() +private suspend fun runServersTest(servers: List, m: ChatModel, onUpdated: (List) -> Unit): Map { + val fs: MutableMap = mutableMapOf() val updatedServers = ArrayList(servers) for ((index, server) in servers.withIndex()) { if (server.enabled) { @@ -331,10 +347,10 @@ private suspend fun runServersTest(servers: List, m: ChatModel, onUpd return fs } -private fun saveSMPServers(servers: List, m: ChatModel, afterSave: () -> Unit = {}) { +private fun saveServers(protocol: ServerProtocol, currServers: MutableState>, servers: List, 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() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt similarity index 89% rename from apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt index 3844a712d7..525263a27b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt @@ -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() diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 80eb68bdfe..b59775407c 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -60,7 +60,11 @@ Error saving SMP servers + Error saving XFTP servers Make sure SMP server addresses are in correct format, line separated and are not duplicated. + Make sure XFTP server addresses are in correct format, line separated and are not duplicated. + Error loading SMP servers + Error loading XFTP servers Error updating network configuration Failed to load chat Failed to load chats @@ -96,12 +100,18 @@ Error changing address Test failed at step %s. Server requires authorization to create queues, check password + Server requires authorization to upload, check password Possibly, certificate fingerprint in server address is incorrect Connect + Disconnect Create queue Secure queue Delete queue - Disconnect + Create file + Upload file + Download file + Compare file + Delete file Error deleting user profile Error updating user privacy @@ -476,12 +486,14 @@ Delete server The servers for new connections of your current chat profile Save servers? + XFTP servers Install SimpleX Chat for terminal Star on GitHub Contribute Rate the app Use SimpleX Chat servers? Your SMP servers + Your XFTP servers Using SimpleX Chat servers. How to How to use your servers @@ -732,7 +744,7 @@ LANGUAGE APP ICON THEMES - MESSAGES + MESSAGES AND FILES CALLS Incognito mode EXPERIMENTAL diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 2bac9953e0..75c2ed540b 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -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))")