move protocol servers view

This commit is contained in:
Diogo
2024-11-20 14:24:01 +00:00
parent 068f3d896b
commit 3f1113f856
4 changed files with 386 additions and 423 deletions
@@ -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
@@ -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<UserServer>()) }
var servers by remember { stateGetOrPut("servers") { emptyList<UserServer>() } }
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<UserServer>,
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<UserServer>, 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<UserServer>): Boolean = servers.all { srv ->
address.hostnames.all { host ->
srv.id == s.id || !srv.server.contains(host)
}
}
private fun hasAllPresets(presetServers: List<UserServer>, servers: List<UserServer>, m: ChatModel): Boolean =
presetServers.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(rhId: Long?, presetServers: List<UserServer>, servers: List<UserServer>, m: ChatModel): List<UserServer> {
val toAdd = ArrayList<UserServer>()
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(srv)
}
}
return toAdd
}
private fun hasPreset(srv: UserServer, servers: List<UserServer>): Boolean =
servers.any { it.server == srv.server }
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> 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<UserServer>): List<UserServer> {
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<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> Unit): Map<String, ProtocolTestFailure> {
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
val updatedServers = ArrayList<UserServer>(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<List<UserServer>>, servers: List<UserServer>, 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,
)
}
@@ -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
)
}
@@ -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<UserServer>()) }
var servers by remember { stateGetOrPut("servers") { emptyList<UserServer>() } }
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<List<UserOperatorServers>>, operatorIndex: Int) {
@@ -23,37 +185,224 @@ fun ModalData.YourServersView(userServers: MutableState<List<UserOperatorServers
}
@Composable
fun ProtocolServerViewLink(
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
server: UserServer,
private fun ProtocolServersLayout(
serverProtocol: ServerProtocol,
duplicateHosts: Set<String>,
rhId: Long?,
testing: Boolean,
servers: List<UserServer>,
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<UserServer>, 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<String>) {
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
)
}
}
@Composable
fun InvalidServer() {
Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error)
}
private fun uniqueAddress(s: UserServer, address: ServerAddress, servers: List<UserServer>): Boolean = servers.all { srv ->
address.hostnames.all { host ->
srv.id == s.id || !srv.server.contains(host)
}
}
private fun hasAllPresets(presetServers: List<UserServer>, servers: List<UserServer>, m: ChatModel): Boolean =
presetServers.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(rhId: Long?, presetServers: List<UserServer>, servers: List<UserServer>, m: ChatModel): List<UserServer> {
val toAdd = ArrayList<UserServer>()
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(srv)
}
}
return toAdd
}
private fun hasPreset(srv: UserServer, servers: List<UserServer>): Boolean =
servers.any { it.server == srv.server }
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> 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<UserServer>): List<UserServer> {
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<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> Unit): Map<String, ProtocolTestFailure> {
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
val updatedServers = ArrayList<UserServer>(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<List<UserServer>>, servers: List<UserServer>, 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,
)
}