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 4e3ee2340c..a1ec9734c9 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,7 @@ 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.ProtocolServersView +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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt deleted file mode 100644 index 1c700cf53e..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ /dev/null @@ -1,383 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionDividerSpaced -import SectionItemView -import SectionTextFooter -import SectionView -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -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 dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.* -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.views.usersettings.networkAndServers.* -import chat.simplex.res.MR - -@Composable -fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { - var presetServers by remember(rhId) { mutableStateOf(emptyList()) } - var servers by remember { stateGetOrPut("servers") { emptyList() } } - var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } - val currServers = remember(rhId) { mutableStateOf(servers) } - val testing = rememberSaveable(rhId) { mutableStateOf(false) } - val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } - val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } - val saveDisabled = remember(servers) { - derivedStateOf { - servers.isEmpty() || - servers == currServers.value || - testing.value || - servers.none { srv -> - val address = parseServerAddress(srv.server) - address != null && uniqueAddress(srv, address, servers) - } || - allServersDisabled.value - } - } - - KeyChangeEffect(rhId) { - servers = emptyList() - serversAlreadyLoaded = false - } - - LaunchedEffect(rhId) { - withApi { -// val res = m.controller.getUserServers(rhId) -// if (res != null) { -// currServers.value = res.protoServers -// presetServers = res.presetServers -// if (servers.isEmpty() && !serversAlreadyLoaded) { -// servers = currServers.value -// serversAlreadyLoaded = true -// } -// } - } - } - val testServersJob = CancellableOnGoneJob() - fun showServer(server: UserServer) { - ModalManager.start.showModalCloseable(true) { close -> - var old by remember { mutableStateOf(server) } - val index = servers.indexOf(old) - ProtocolServerView( - m, - old, - serverProtocol, - onUpdate = { updated -> - val newServers = ArrayList(servers) - newServers.removeAt(index) - newServers.add(index, updated) - old = updated - servers = newServers - }, - onDelete = { - val newServers = ArrayList(servers) - newServers.removeAt(index) - servers = newServers - close() - }) - } - } - ModalView( - close = { - if (saveDisabled.value) close() - else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close) - }, - ) { - ProtocolServersLayout( - serverProtocol, - testing = testing.value, - servers = servers, - serversUnchanged = serversUnchanged.value, - saveDisabled = saveDisabled.value, - allServersDisabled = allServersDisabled.value, - m.currentUser.value, - addServer = { - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.smp_servers_add), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - servers = servers + UserServer.empty - // No saving until something will be changed on the next screen to prevent blank servers on the list - showServer(servers.last()) - }) { - 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() - servers = servers + it - } - } - } - ) { - 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) - } - } - } - } - ) - }, - testServers = { - testServersJob.value = withLongRunningApi { - testServers(testing, servers, m) { - servers = it - } - } - }, - resetServers = { - servers = currServers.value - }, - saveSMPServers = { - saveServers(rhId, serverProtocol, currServers, servers, m) - }, - showServer = ::showServer, - ) - - 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 ProtocolServersLayout( - serverProtocol: ServerProtocol, - testing: Boolean, - servers: List, - serversUnchanged: Boolean, - saveDisabled: Boolean, - allServersDisabled: Boolean, - currentUser: User?, - addServer: () -> Unit, - testServers: () -> Unit, - resetServers: () -> Unit, - saveSMPServers: () -> Unit, - showServer: (UserServer) -> Unit, -) { - ColumnWithScrollBar { - AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) - - val configuredServers = servers.filter { it.preset || it.enabled } - val otherServers = servers.filter { !(it.preset || it.enabled) } - - if (configuredServers.isNotEmpty()) { - SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_configured else MR.strings.xftp_servers_configured).uppercase()) { - for (srv in configuredServers) { - SectionItemView({ showServer(srv) }, disabled = testing) { - ProtocolServerView(serverProtocol, srv, servers, testing) - } - } - } - SectionTextFooter( - remember(currentUser?.displayName) { - buildAnnotatedString { - append(generalGetString(MR.strings.smp_servers_per_user) + " ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(currentUser?.displayName ?: "") - } - append(".") - } - } - ) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - } - - if (otherServers.isNotEmpty()) { - SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_other else MR.strings.xftp_servers_other).uppercase()) { - for (srv in otherServers.filter { !(it.preset || it.enabled) }) { - SectionItemView({ showServer(srv) }, disabled = testing) { - ProtocolServerView(serverProtocol, srv, servers, testing) - } - } - } - } - - SectionView { - SettingsActionItem( - painterResource(MR.images.ic_add), - stringResource(MR.strings.smp_servers_add), - addServer, - 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 { - SectionItemView(resetServers, disabled = serversUnchanged) { - Text(stringResource(MR.strings.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - val testServersDisabled = testing || allServersDisabled - SectionItemView(testServers, disabled = testServersDisabled) { - Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - SectionItemView(saveSMPServers, disabled = saveDisabled) { - Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - } - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - HowToButton() - } - SectionBottomSpacer() - } -} - -@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() - !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) - else -> ShowTestStatus(srv) - } - Spacer(Modifier.padding(horizontal = 4.dp)) - val text = address?.hostnames?.firstOrNull() ?: srv.server - if (srv.enabled) { - Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1) - } else { - Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) - } -} - -@Composable -private fun HowToButton() { - val uriHandler = LocalUriHandler.current - SettingsActionItem( - painterResource(MR.images.ic_open_in_new), - stringResource(MR.strings.how_to_use_your_servers), - { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary - ) -} - -@Composable -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) - 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(MR.strings.smp_servers_test_failed), - text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg - ) - } -} - -private fun resetTestStatus(servers: List): List { - 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, m: ChatModel, onUpdated: (List) -> Unit): Map { - val fs: MutableMap = mutableMapOf() - val updatedServers = ArrayList(servers) - for ((index, server) in servers.withIndex()) { - if (server.enabled) { - interruptIfCancelled() - 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.server)] = f - } - } - } - return fs -} - -private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState>, servers: List, m: ChatModel, afterSave: () -> Unit = {}) { - withBGApi { -// if (m.controller.setUserServers(rhId, protocol, servers)) { -// currServers.value = servers -// } - afterSave() - } -} - -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { - AlertManager.shared.showAlertDialogStacked( - title = generalGetString(MR.strings.smp_save_servers_question), - confirmText = generalGetString(MR.strings.save_verb), - dismissText = generalGetString(MR.strings.exit_without_saving), - onConfirm = save, - onDismiss = revert, - ) -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 11ceef121c..81c8e5a4b0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -161,12 +161,9 @@ fun OperatorViewLayout( SectionView(generalGetString(MR.strings.message_servers).uppercase()) { userServers.value[operatorIndex].smpServers.forEach { server -> SectionItemView { - ProtocolServerViewLink( - userServers = userServers, - serverErrors = serverErrors, - server = server, + ProtocolServerView( + srv = server, serverProtocol = ServerProtocol.SMP, - rhId = rhId, duplicateHosts = duplicateHosts ) } 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 83201413cc..f6ca25ca73 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,19 +1,181 @@ package chat.simplex.common.views.usersettings.networkAndServers +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import chat.simplex.common.model.* -import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.views.helpers.* -import chat.simplex.res.MR +import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.res.MR + +@Composable +fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { + var presetServers by remember(rhId) { mutableStateOf(emptyList()) } + var servers by remember { stateGetOrPut("servers") { emptyList() } } + var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } + val currServers = remember(rhId) { mutableStateOf(servers) } + val testing = rememberSaveable(rhId) { mutableStateOf(false) } + val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } + val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } + val saveDisabled = remember(servers) { + derivedStateOf { + servers.isEmpty() || + servers == currServers.value || + testing.value || + servers.none { srv -> + val address = parseServerAddress(srv.server) + address != null && uniqueAddress(srv, address, servers) + } || + allServersDisabled.value + } + } + + KeyChangeEffect(rhId) { + servers = emptyList() + serversAlreadyLoaded = false + } + + LaunchedEffect(rhId) { + withApi { +// val res = m.controller.getUserServers(rhId) +// if (res != null) { +// currServers.value = res.protoServers +// presetServers = res.presetServers +// if (servers.isEmpty() && !serversAlreadyLoaded) { +// servers = currServers.value +// serversAlreadyLoaded = true +// } +// } + } + } + val testServersJob = CancellableOnGoneJob() + fun showServer(server: UserServer) { + ModalManager.start.showModalCloseable(true) { close -> + var old by remember { mutableStateOf(server) } + val index = servers.indexOf(old) + ProtocolServerView( + m, + old, + serverProtocol, + onUpdate = { updated -> + val newServers = ArrayList(servers) + newServers.removeAt(index) + newServers.add(index, updated) + old = updated + servers = newServers + }, + onDelete = { + val newServers = ArrayList(servers) + newServers.removeAt(index) + servers = newServers + close() + }) + } + } + ModalView( + close = { + if (saveDisabled.value) close() + else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close) + }, + ) { + ProtocolServersLayout( + serverProtocol, + testing = testing.value, + servers = servers, + serversUnchanged = serversUnchanged.value, + saveDisabled = saveDisabled.value, + allServersDisabled = allServersDisabled.value, + m.currentUser.value, + addServer = { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.smp_servers_add), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + servers = servers + UserServer.empty + // No saving until something will be changed on the next screen to prevent blank servers on the list + showServer(servers.last()) + }) { + 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() + servers = servers + it + } + } + } + ) { + 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) + } + } + } + } + ) + }, + testServers = { + testServersJob.value = withLongRunningApi { + testServers(testing, servers, m) { + servers = it + } + } + }, + resetServers = { + servers = currServers.value + }, + saveSMPServers = { + saveServers(rhId, serverProtocol, currServers, servers, m) + }, + showServer = ::showServer, + ) + + 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 fun ModalData.YourServersView(userServers: MutableState>, operatorIndex: Int) { @@ -23,37 +185,224 @@ fun ModalData.YourServersView(userServers: MutableState>, - serverErrors: MutableState>, - server: UserServer, +private fun ProtocolServersLayout( serverProtocol: ServerProtocol, - duplicateHosts: Set, - rhId: Long?, + testing: Boolean, + servers: List, + serversUnchanged: Boolean, + saveDisabled: Boolean, + allServersDisabled: Boolean, + currentUser: User?, + addServer: () -> Unit, + testServers: () -> Unit, + resetServers: () -> Unit, + saveSMPServers: () -> Unit, + showServer: (UserServer) -> Unit, ) { - val address = parseServerAddress(server.server) - when { - address == null || !address.valid || address.serverProtocol != serverProtocol || address.hostnames.any { it in duplicateHosts } -> InvalidServer() - !server.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) - else -> ShowTestStatus(server) + ColumnWithScrollBar { + AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) + + val configuredServers = servers.filter { it.preset || it.enabled } + val otherServers = servers.filter { !(it.preset || it.enabled) } + + if (configuredServers.isNotEmpty()) { + SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_configured else MR.strings.xftp_servers_configured).uppercase()) { + for (srv in configuredServers) { + SectionItemView({ showServer(srv) }, disabled = testing) { + ProtocolServerView(serverProtocol, srv, servers, testing) + } + } + } + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.smp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) } - Spacer(Modifier.padding(horizontal = 4.dp)) - val text = address?.hostnames?.firstOrNull() ?: server.server - if (server.enabled) { - Text(text, color = MaterialTheme.colors.onBackground, maxLines = 1) - } else { - Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) + + if (otherServers.isNotEmpty()) { + SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_other else MR.strings.xftp_servers_other).uppercase()) { + for (srv in otherServers.filter { !(it.preset || it.enabled) }) { + SectionItemView({ showServer(srv) }, disabled = testing) { + ProtocolServerView(serverProtocol, srv, servers, testing) + } + } + } } + + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.smp_servers_add), + addServer, + 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 { + SectionItemView(resetServers, disabled = serversUnchanged) { + Text(stringResource(MR.strings.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } + val testServersDisabled = testing || allServersDisabled + SectionItemView(testServers, disabled = testServersDisabled) { + Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } + SectionItemView(saveSMPServers, disabled = saveDisabled) { + Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } + } + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + HowToButton() + } + SectionBottomSpacer() + } } @Composable -private fun InvalidServer() { - Icon( - painterResource(MR.images.ic_error), - contentDescription = stringResource(MR.strings.server_error), - tint = Color.Red, - modifier = Modifier - .size(19.sp.toDp()) - .offset(x = 2.sp.toDp()) +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() + !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) + else -> ShowTestStatus(srv) + } + Spacer(Modifier.padding(horizontal = 4.dp)) + val text = address?.hostnames?.firstOrNull() ?: srv.server + if (srv.enabled) { + Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1) + } else { + Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) + } +} + +// TODO: Remove duplicate. +@Composable +fun ProtocolServerView(serverProtocol: ServerProtocol, srv: UserServer, duplicateHosts: Set) { + val address = parseServerAddress(srv.server) + when { + address == null || !address.valid || address.serverProtocol != serverProtocol || address.hostnames.any { it in duplicateHosts } -> InvalidServer() + !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) + else -> ShowTestStatus(srv) + } + Spacer(Modifier.padding(horizontal = 4.dp)) + val text = address?.hostnames?.firstOrNull() ?: srv.server + if (srv.enabled) { + Text(text, color = MaterialTheme.colors.onBackground, maxLines = 1) + } else { + Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) + } +} + +@Composable +private fun HowToButton() { + val uriHandler = LocalUriHandler.current + SettingsActionItem( + painterResource(MR.images.ic_open_in_new), + stringResource(MR.strings.how_to_use_your_servers), + { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary ) -} \ No newline at end of file +} + +@Composable +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) + 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(MR.strings.smp_servers_test_failed), + text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg + ) + } +} + +private fun resetTestStatus(servers: List): List { + 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, m: ChatModel, onUpdated: (List) -> Unit): Map { + val fs: MutableMap = mutableMapOf() + val updatedServers = ArrayList(servers) + for ((index, server) in servers.withIndex()) { + if (server.enabled) { + interruptIfCancelled() + 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.server)] = f + } + } + } + return fs +} + +private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState>, servers: List, m: ChatModel, afterSave: () -> Unit = {}) { + withBGApi { +// if (m.controller.setUserServers(rhId, protocol, servers)) { +// currServers.value = servers +// } + afterSave() + } +} + +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.smp_save_servers_question), + confirmText = generalGetString(MR.strings.save_verb), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +}