diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index af181f4d87..c7ca67c9d5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -3861,7 +3861,6 @@ sealed class UserServersError { @Serializable @SerialName("noServers") data class NoServers(val protocol: ServerProtocol, val user: UserRef?): UserServersError() @Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError() @Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError() - @Serializable @SerialName("invalidServer") data class InvalidServer(val protocol: ServerProtocol, val invalidServer: String): UserServersError() @Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError() val globalError: String? @@ -3876,7 +3875,6 @@ sealed class UserServersError { is NoServers -> this.protocol is StorageMissing -> this.protocol is ProxyMissing -> this.protocol - is InvalidServer -> this.protocol is DuplicateServer -> this.protocol } val globalSMPError: String? diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index a1ec9734c9..acbc72ff48 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -48,7 +48,6 @@ import chat.simplex.common.model.localTimestamp import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.networkAndServers.ProtocolServersView import chat.simplex.common.views.usersettings.SettingsPreferenceItem import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -540,15 +539,8 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, ) ) } - if (summary.known == true) { - SectionItemView(click = { - ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) } - }) { - Text(generalGetString(MR.strings.open_server_settings_button)) - } - if (summary.stats != null || summary.sessions != null) { - SectionDividerSpaced() - } + if (summary.stats != null || summary.sessions != null) { + SectionDividerSpaced() } if (summary.stats != null) { @@ -579,12 +571,7 @@ fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, r ) ) } - if (summary.known == true) { - SectionItemView(click = { - ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) } - }) { - Text(generalGetString(MR.strings.open_server_settings_button)) - } + if (summary.stats != null || summary.subs != null || summary.sessions != null) { SectionDividerSpaced() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index f076a91bbb..52301c9577 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -188,7 +188,17 @@ fun ModalData.NetworkAndServersView(close: () -> Unit) { val nullOperatorIndex = userServers.value.indexOfFirst { it.operator == null } if (nullOperatorIndex != -1) { - SectionItemView({ ModalManager.start.showModal { YourServersView(userServers = userServers, operatorIndex = nullOperatorIndex) } }) { + SectionItemView({ + ModalManager.start.showModal { + YourServersView( + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = nullOperatorIndex, + rhId = currentRemoteHost?.remoteHostId + ) + } + }) { Icon( painterResource(MR.images.ic_dns), stringResource(MR.strings.your_servers), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt new file mode 100644 index 0000000000..2683267926 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt @@ -0,0 +1,248 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionItemViewSpaceBetween +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.platform.* +import chat.simplex.common.views.usersettings.PreferenceToggle +import chat.simplex.res.MR +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.distinctUntilChanged + +@Composable +fun NewServerView( + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long?, + close: () -> Unit +) { + val newServer = remember { mutableStateOf(UserServer.empty) } + val testing = remember { mutableStateOf(false) } + + BackHandler(onBack = { + addServer( + newServer.value, + userServers, + serverErrors, + rhId, + close = close + ) + }) + + NewServerLayout( + userServers, + serverErrors, + operatorIndex, + rhId, + newServer, + testing +// testServer = { +// testing = true +// withLongRunningApi { +// val res = testServerConnection(server, m) +// if (isActive) { +// onUpdate(res.first) +// testing = false +// } +// } +// } + ) + if (testing.value) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) + } + } +} + +@Composable +private fun NewServerLayout( + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long?, + server: MutableState, + testing: MutableState +) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.smp_servers_new_server)) + CustomServer(userServers, serverErrors, operatorIndex, rhId, server, testing) + SectionBottomSpacer() + } +} + +@Composable +private fun CustomServer( + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long?, + server: MutableState, + testing: MutableState +) { + val serverAddress = remember { mutableStateOf(server.value.server) } + val valid = remember { + derivedStateOf { + with(parseServerAddress(serverAddress.value)) { + this?.valid == true + } + } + } + SectionView( + stringResource(MR.strings.smp_servers_your_server_address).uppercase(), + icon = painterResource(MR.images.ic_error), + iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent, + ) { + val testedPreviously = remember { mutableMapOf() } + TextEditor( + serverAddress, + Modifier.height(144.dp) + ) + // TODO review + LaunchedEffect(Unit) { + snapshotFlow { server.value.server } + .distinctUntilChanged() + .collect { + testedPreviously[server.value.server] = server.value.tested + // server = server.copy(server = it, tested = testedPreviously[serverAddress.value]) + } + } + } + SectionDividerSpaced(maxTopPadding = true) + // UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete) + + if (valid.value) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) { + QRCode(serverAddress.value) + } + } +} + +fun serverProtocolAndOperator( + server: UserServer, + userServers: List +): Pair? { + val serverAddress = parseServerAddress(server.server) + return if (serverAddress != null) { + val serverProtocol = serverAddress.serverProtocol + val hostnames = serverAddress.hostnames + val matchingOperator = userServers.mapNotNull { it.operator }.firstOrNull { op -> + op.serverDomains.any { domain -> + hostnames.any { hostname -> + hostname.endsWith(domain) + } + } + } + Pair(serverProtocol, matchingOperator) + } else { + null + } +} + +fun addServer( + server: UserServer, + userServers: MutableState>, + serverErrors: MutableState>, + rhId: Long?, + close: () -> Unit +) { + val result = serverProtocolAndOperator(server, userServers.value) + if (result != null) { + val (serverProtocol, matchingOperator) = result + val operatorIndex = userServers.value.indexOfFirst { it.operator?.operatorId == matchingOperator?.operatorId } + if (operatorIndex != -1) { + // Create a mutable copy of the userServers list + val updatedUserServers = userServers.value.toMutableList() + val operatorServers = updatedUserServers[operatorIndex] + + // Create a mutable copy of the smpServers or xftpServers and add the server + when (serverProtocol) { + ServerProtocol.SMP -> { + val updatedSMPServers = operatorServers.smpServers.toMutableList() + updatedSMPServers.add(server) + updatedUserServers[operatorIndex] = operatorServers.copy(smpServers = updatedSMPServers) + } + ServerProtocol.XFTP -> { + val updatedXFTPServers = operatorServers.xftpServers.toMutableList() + updatedXFTPServers.add(server) + updatedUserServers[operatorIndex] = operatorServers.copy(xftpServers = updatedXFTPServers) + } + } + + userServers.value = updatedUserServers + // TODO + // validateServers(rhId, userServers, serverErrors) + close() + matchingOperator?.let { op -> + AlertManager.shared.showAlertMsg( + title = "Operator server", + text = "Server added to operator ${op.tradeName}." + ) + } + } else { // Shouldn't happen + close() + AlertManager.shared.showAlertMsg(title = "Error adding server") + } + } else { + close() + if (server.server.trim().isNotEmpty()) { + AlertManager.shared.showAlertMsg( + title = "Invalid server address!", + text = "Check server address and try again." + ) + } + } +} + +//@Composable +//private fun UseServerSection( +// valid: Boolean, +// testing: MutableState, +// server: UserServer +//) { +// SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) { +// SectionItemViewSpaceBetween(testServer, disabled = !valid || testing.value) { +// Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing.value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) +// ShowTestStatus(server) +// } +// +// val enabled = rememberUpdatedState(server.enabled) +// PreferenceToggle( +// stringResource(MR.strings.smp_servers_use_server_for_new_conn), +// disabled = server.tested != true && !server.preset, +// checked = enabled.value +// ) { +// onUpdate(server.copy(enabled = it)) +// } +// +// SectionItemView(onDelete, disabled = testing) { +// Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) +// } +// } +//} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt index f6ca25ca73..4a5b0b2322 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings.networkAndServers import SectionBottomSpacer +import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionTextFooter @@ -42,7 +43,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser testing.value || servers.none { srv -> val address = parseServerAddress(srv.server) - address != null && uniqueAddress(srv, address, servers) + address != null } || allServersDisabled.value } @@ -131,15 +132,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - val hasAllPresets = hasAllPresets(presetServers, servers, m) - if (!hasAllPresets) { - SectionItemView({ - AlertManager.shared.hideAlert() - servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset } - }) { - Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } } } ) @@ -177,13 +169,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser } } -@Composable -fun ModalData.YourServersView(userServers: MutableState>, operatorIndex: Int) { - ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.your_servers)) - } -} - @Composable private fun ProtocolServersLayout( serverProtocol: ServerProtocol, @@ -269,11 +254,127 @@ private fun ProtocolServersLayout( } } +@Composable +fun ModalData.YourServersView( + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.your_servers)) + YourServersViewLayout(currUserServers, userServers, serverErrors, operatorIndex, rhId, testing.value) + if (testing.value) { + DefaultProgressView(null) + } + } +} + +@Composable +fun YourServersViewLayout( + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long?, + testing: Boolean +) { + val scope = rememberCoroutineScope() + val duplicateHosts = findDuplicateHosts(serverErrors.value) + + Column { + if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { + SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEach { server -> + SectionItemView { + ProtocolServerView( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + val smpErrors = globalSMPServersError(serverErrors.value) + if (smpErrors != null) { + SectionCustomFooter { + ServerErrorsView(smpErrors) + } + } + } + + SectionDividerSpaced() + + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.smp_servers_add), + click = { showAddServerDialog(userServers, serverErrors, operatorIndex, rhId) }, + disabled = testing, + textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + + SectionView { + // TODO Test servers button + SectionItemView { Text("Test servers") } + } + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + HowToButton() + } + SectionBottomSpacer() + } +} + +fun showAddServerDialog( + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.smp_servers_add), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showModalCloseable { close -> + NewServerView(userServers, serverErrors, operatorIndex, rhId, close) + } + }) { + Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (appPlatform.isAndroid) { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showModalCloseable { close -> + ScanProtocolServer(rhId) { + close() + // TODO reuse logic from NewServerView + } + } + } + ) { + Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + } + ) +} + @Composable private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: UserServer, servers: List, disabled: Boolean) { val address = parseServerAddress(srv.server) when { - address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer() + address == null || !address.valid || address.serverProtocol != serverProtocol -> InvalidServer() !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) else -> ShowTestStatus(srv) } @@ -321,28 +422,6 @@ fun InvalidServer() { Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) } -private fun uniqueAddress(s: UserServer, address: ServerAddress, servers: List): Boolean = servers.all { srv -> - address.hostnames.all { host -> - srv.id == s.id || !srv.server.contains(host) - } -} - -private fun hasAllPresets(presetServers: List, servers: List, m: ChatModel): Boolean = - presetServers.all { hasPreset(it, servers) } ?: true - -private fun addAllPresets(rhId: Long?, presetServers: List, servers: List, m: ChatModel): List { - val toAdd = ArrayList() - for (srv in presetServers) { - if (!hasPreset(srv, servers)) { - toAdd.add(srv) - } - } - return toAdd -} - -private fun hasPreset(srv: UserServer, servers: List): Boolean = - servers.any { it.server == srv.server } - private suspend fun testServers(testing: MutableState, servers: List, m: ChatModel, onUpdated: (List) -> Unit) { val resetStatus = resetTestStatus(servers) onUpdated(resetStatus) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 0476e3793b..9d6c36bb10 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -757,6 +757,7 @@ Some servers failed the test: Scan server QR code Enter server manually + New server Preset server Your server Your server address