mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 09:44:15 +00:00
core, ui: per-server roles for self-hosted servers (#7254)
* core, ui: plan per-server roles for self-hosted servers * core: add per-server roles field to UserServer * core: add nullable role columns to protocol_servers * core: persist per-server roles * core: validate server coverage using per-server roles * test: cover per-server roles resolution and coverage * multiplatform: per-server role toggles for self-hosted servers * ios: per-server role toggles for self-hosted servers * core: per-server role overrides with per-role defaults * test: cover three-state per-server role resolution * fix: derive Eq for ServerRolesOverride * multiplatform: three-state role dropdowns on saved servers * ios: three-state role pickers on saved servers * test: per-server roles independent across two servers * test: enable names role in name-resolution tests * style: trim comments in per-server roles code * chore: rename server_roles migration to 20260716 (last) * core: per-server roles override operator roles, inherit when unset * multiplatform: per-server role default inherits from operator * ios: per-server role default inherits from operator * refactor(servers): tidy per-server roles per review - dedup no-operator default into ServerRoles.noOperatorDefault (Kotlin/Swift) - iOS: move roles-section control flow to the call site via a named gate, and parse the server address once instead of up to three times - Kotlin: collapse redundant derivedState; revert defaultOn->default rename for cross-platform parity - drop unused Hashable conformance on Swift ServerRoles - add agentServerCfgs test for names inheritance from an operator - remove no-op enableNamesRole calls from dormant DirectoryTests - fix ChatClient import ordering * chore(migration): date server_roles migration 20260720 * only show roles when server is enabled, move section above QR code --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
+16
-3
@@ -4647,6 +4647,18 @@ data class ServerRoles(
|
||||
val storage: Boolean,
|
||||
val proxy: Boolean,
|
||||
val names: Boolean
|
||||
) {
|
||||
companion object {
|
||||
// roles applied when a server matches no operator, mirrors core resolveServerRoles (Operators.hs)
|
||||
val noOperatorDefault = ServerRoles(storage = true, proxy = true, names = false)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerRolesOverride(
|
||||
val storage: Boolean? = null,
|
||||
val proxy: Boolean? = null,
|
||||
val names: Boolean? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -4668,8 +4680,8 @@ data class UserOperatorServers(
|
||||
serverDomains = emptyList(),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
|
||||
enabled = false,
|
||||
smpRoles = ServerRoles(storage = true, proxy = true, names = true),
|
||||
xftpRoles = ServerRoles(storage = true, proxy = true, names = false)
|
||||
smpRoles = ServerRoles.noOperatorDefault,
|
||||
xftpRoles = ServerRoles.noOperatorDefault
|
||||
)
|
||||
|
||||
companion object {
|
||||
@@ -4801,7 +4813,8 @@ data class UserServer(
|
||||
val preset: Boolean,
|
||||
val tested: Boolean? = null,
|
||||
val enabled: Boolean,
|
||||
val deleted: Boolean
|
||||
val deleted: Boolean,
|
||||
val roles: ServerRolesOverride = ServerRolesOverride(),
|
||||
) {
|
||||
@Transient
|
||||
private val createdAt: Date = Date()
|
||||
|
||||
+45
-8
@@ -19,6 +19,7 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -88,6 +89,7 @@ fun ProtocolServerView(
|
||||
ProtocolServerLayout(
|
||||
draftServer,
|
||||
serverProtocol,
|
||||
userServers,
|
||||
testing.value,
|
||||
testServer = {
|
||||
testing.value = true
|
||||
@@ -120,6 +122,7 @@ fun ProtocolServerView(
|
||||
private fun ProtocolServerLayout(
|
||||
server: MutableState<UserServer>,
|
||||
serverProtocol: ServerProtocol,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
testing: Boolean,
|
||||
testServer: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
@@ -130,7 +133,7 @@ private fun ProtocolServerLayout(
|
||||
if (server.value.preset) {
|
||||
PresetServer(server, testing, testServer)
|
||||
} else {
|
||||
CustomServer(server, testing, testServer, onDelete)
|
||||
CustomServer(server, testing, testServer, onDelete, serverProtocol, userServers)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
@@ -164,15 +167,11 @@ fun CustomServer(
|
||||
testing: Boolean,
|
||||
testServer: () -> Unit,
|
||||
onDelete: (() -> Unit)?,
|
||||
serverProtocol: ServerProtocol? = null,
|
||||
userServers: MutableState<List<UserOperatorServers>>? = null
|
||||
) {
|
||||
val serverAddress = remember { mutableStateOf(server.value.server) }
|
||||
val valid = remember {
|
||||
derivedStateOf {
|
||||
with(parseServerAddress(serverAddress.value)) {
|
||||
this?.valid == true
|
||||
}
|
||||
}
|
||||
}
|
||||
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
|
||||
SectionView(
|
||||
stringResource(MR.strings.smp_servers_your_server_address),
|
||||
icon = painterResource(MR.images.ic_error),
|
||||
@@ -196,6 +195,12 @@ fun CustomServer(
|
||||
|
||||
UseServerSection(server, valid.value, testing, testServer, onDelete)
|
||||
|
||||
val op = remember(server.value.server) { serverProtocolAndOperator(server.value, userServers?.value ?: listOf())?.second }
|
||||
if (serverProtocol == ServerProtocol.SMP && server.value.enabled && (!server.value.preset || server.value.roles != ServerRolesOverride())) {
|
||||
SectionDividerSpaced()
|
||||
ServerRolesSection(server, op?.smpRoles ?: ServerRoles.noOperatorDefault)
|
||||
}
|
||||
|
||||
if (valid.value) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.smp_servers_add_to_another_device)) {
|
||||
@@ -204,6 +209,38 @@ fun CustomServer(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerRolesSection(server: MutableState<UserServer>, inherited: ServerRoles) {
|
||||
SectionView(stringResource(MR.strings.operator_use_for_messages)) {
|
||||
RoleDropDown(stringResource(MR.strings.operator_use_for_messages_receiving), server.value.roles.storage, defaultOn = inherited.storage) {
|
||||
server.value = server.value.copy(roles = server.value.roles.copy(storage = it))
|
||||
}
|
||||
RoleDropDown(stringResource(MR.strings.operator_use_for_messages_private_routing), server.value.roles.proxy, defaultOn = inherited.proxy) {
|
||||
server.value = server.value.copy(roles = server.value.roles.copy(proxy = it))
|
||||
}
|
||||
RoleDropDown(stringResource(MR.strings.operator_use_for_names), server.value.roles.names, defaultOn = inherited.names) {
|
||||
server.value = server.value.copy(roles = server.value.roles.copy(names = it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoleDropDown(title: String, value: Boolean?, defaultOn: Boolean, onSelected: (Boolean?) -> Unit) {
|
||||
val values = remember(defaultOn, appPrefs.appLanguage.state.value) {
|
||||
listOf(
|
||||
null to String.format(generalGetString(MR.strings.chat_preferences_default), generalGetString(if (defaultOn) MR.strings.chat_preferences_yes else MR.strings.chat_preferences_no)),
|
||||
true to generalGetString(MR.strings.chat_preferences_yes),
|
||||
false to generalGetString(MR.strings.chat_preferences_no)
|
||||
)
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
title,
|
||||
values,
|
||||
rememberUpdatedState(value),
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseServerSection(
|
||||
server: MutableState<UserServer>,
|
||||
|
||||
Reference in New Issue
Block a user