android: add server view (#5221)

This commit is contained in:
spaced4ndy
2024-11-20 20:49:05 +04:00
committed by GitHub
parent 215d3e4d39
commit 5800cb7958
6 changed files with 382 additions and 59 deletions
@@ -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?
@@ -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()
}
@@ -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),
@@ -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<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
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<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
operatorIndex: Int,
rhId: Long?,
server: MutableState<UserServer>,
testing: MutableState<Boolean>
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.smp_servers_new_server))
CustomServer(userServers, serverErrors, operatorIndex, rhId, server, testing)
SectionBottomSpacer()
}
}
@Composable
private fun CustomServer(
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
operatorIndex: Int,
rhId: Long?,
server: MutableState<UserServer>,
testing: MutableState<Boolean>
) {
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<String, Boolean?>() }
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<UserOperatorServers>
): Pair<ServerProtocol, ServerOperator?>? {
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<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
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<Boolean>,
// 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)
// }
// }
//}
@@ -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<List<UserOperatorServers>>, 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<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
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<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
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<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
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<UserServer>, 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<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)
@@ -757,6 +757,7 @@
<string name="smp_servers_test_some_failed">Some servers failed the test:</string>
<string name="smp_servers_scan_qr">Scan server QR code</string>
<string name="smp_servers_enter_manually">Enter server manually</string>
<string name="smp_servers_new_server">New server</string>
<string name="smp_servers_preset_server">Preset server</string>
<string name="smp_servers_your_server">Your server</string>
<string name="smp_servers_your_server_address">Your server address</string>