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 50e6a5bbea..909fe4aeb2 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 @@ -3702,7 +3702,7 @@ data class ServerOperator( val legalName: String?, val serverDomains: List, val conditionsAcceptance: ConditionsAcceptance, - val enabled: Boolean, + var enabled: Boolean, val smpRoles: ServerRoles, val xftpRoles: ServerRoles, ) { @@ -5285,7 +5285,7 @@ sealed class CR { @Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR() @Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List): CR() @Serializable @SerialName("userServersValidation") class UserServersValidation(val serverErrors: List): CR() - @Serializable @SerialName("usageConditions") class UsageConditions(val usageConditions: UsageConditionsDetail, val conditionsText: String, val acceptedConditions: UsageConditions?): CR() + @Serializable @SerialName("usageConditions") class UsageConditions(val usageConditions: UsageConditionsDetail, val conditionsText: String?, val acceptedConditions: UsageConditions?): CR() @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: UserRef, val chatItemTTL: Long? = null): CR() @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() @Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR() 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 5cfed63fcc..d1c4742a2e 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 @@ -160,7 +160,7 @@ fun NetworkAndServersView(close: () -> Unit) { if (!chatModel.desktopNoUserNoRemote) { SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) { userServers.value.forEachIndexed { index, srv -> - srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers) } + srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, currentRemoteHost?.remoteHostId) } } } if (conditionsAction != null && anyOperatorEnabled.value) { @@ -609,9 +609,10 @@ private fun ServerOperatorRow( index: Int, operator: ServerOperator, currUserServers: MutableState>, - userServers: MutableState> + userServers: MutableState>, + rhId: Long? ) { - SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(index, operator) } }) { + SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorView(currUserServers, userServers, index, rhId) } }) { Image( painterResource(MR.images.decentralized), operator.tradeName, 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 f0e6405a27..0daa7a71e0 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 @@ -1,34 +1,42 @@ package chat.simplex.common.views.usersettings.networkAndServers +import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import TextIconSpaced +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.getUsageConditions import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.launch import kotlinx.datetime.* import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @Composable -fun OperatorView(currUserServers: MutableState>, userServers: MutableState>, operatorIndex: Int) { +fun ModalData.OperatorView(currUserServers: MutableState>, userServers: MutableState>, operatorIndex: Int, rhId: Long?) { val testing = remember { mutableStateOf(false) } + val operator = remember { userServers.value[operatorIndex].operator_ } - Box(Modifier.alpha(if (testing.value) 0.6f else 1f)) { - OperatorViewLayout(currUserServers, userServers, operatorIndex) + ColumnWithScrollBar(Modifier.alpha(if (testing.value) 0.6f else 1f)) { + AppBarTitle(String.format(stringResource(MR.strings.operator_servers_title), operator.tradeName)) + OperatorViewLayout(currUserServers, userServers, operatorIndex, rhId) if (testing.value) { DefaultProgressView(null) } @@ -36,11 +44,24 @@ fun OperatorView(currUserServers: MutableState>, userS } @Composable -fun OperatorViewLayout(currUserServers: MutableState>, userServers: MutableState>, operatorIndex: Int) { +fun OperatorViewLayout(currUserServers: MutableState>, userServers: MutableState>, operatorIndex: Int, rhId: Long?) { val operator = remember { userServers.value[operatorIndex].operator_ } Column { - SectionView(generalGetString(MR.strings.operator)) { + SectionView(generalGetString(MR.strings.operator).uppercase()) { + SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) { + Image( + painterResource(MR.images.decentralized), + operator.tradeName, + modifier = Modifier.size(24.dp), + colorFilter = if (operator.enabled) null else ColorFilter.colorMatrix(ColorMatrix().apply { + setToSaturation(0f) + }) + ) + TextIconSpaced() + Text(operator.tradeName, color = MaterialTheme.colors.onBackground) + } + UseOperatorToggle(currUserServers = currUserServers, userServers = userServers, operatorIndex = operatorIndex, rhId = rhId) } if (operator.enabled) { if (userServers.value[operatorIndex].smpServers.filter { !it.deleted }.isNotEmpty()) { @@ -51,17 +72,162 @@ fun OperatorViewLayout(currUserServers: MutableState>, } @Composable -fun ModalData.OperatorInfoView(index: Int, serverOperator: ServerOperator) { +private fun OperatorInfoView(serverOperator: ServerOperator) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.operator_info_title)) - SectionView(generalGetString(MR.strings.operator_description)) { - Text(serverOperator.info.description) + SectionView(generalGetString(MR.strings.operator_description).uppercase()) { + SectionItemView { + Text(serverOperator.info.description) + } } - SectionView(generalGetString(MR.strings.operator_website)) { - val website = serverOperator.info.website - val uriHandler = LocalUriHandler.current - Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_website).uppercase()) { + SectionItemView { + val website = serverOperator.info.website + val uriHandler = LocalUriHandler.current + Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) + } } } +} + +@Composable +private fun UseOperatorToggle( + currUserServers: MutableState>, + userServers: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + val operator = remember { userServers.value[operatorIndex].operator_ } + SectionItemView { + Text( + stringResource(MR.strings.operator_use_operator_toggle_description), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = operator.enabled, + onCheckedChange = { enabled -> + if (enabled) { + when (operator.conditionsAcceptance) { + is ConditionsAcceptance.Accepted -> { + userServers.value[operatorIndex].operator_.enabled = true + } + + is ConditionsAcceptance.Required -> { + if (operator.conditionsAcceptance.deadline == null) { + val deadline = operator.conditionsAcceptance.deadline + // show conditions modal + + ModalManager.start.showModalCloseable { _ -> + SingleOperatorUsageConditionsView( + currUserServers = currUserServers, + userServers = userServers, + operatorIndex = operatorIndex, + rhId = rhId + ) + } + } else { + userServers.value[operatorIndex].operator_.enabled = true + } + } + } + } else { + userServers.value[operatorIndex].operator_.enabled = false + } + }, + ) + } +} + +@Composable +private fun SingleOperatorUsageConditionsView( + currUserServers: MutableState>, + userServers: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } + val operator = remember { userServers.value[operatorIndex].operator_ } + + ColumnWithScrollBar { + AppBarTitle(String.format(stringResource(MR.strings.use_operator_x), operator.tradeName)) + + if (operator.conditionsAcceptance is ConditionsAcceptance.Accepted) { + // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted + ConditionsTextView(rhId) + } else if (operatorsWithConditionsAccepted.isNotEmpty()) { + // + } else { + // + } + } +} + +@Composable +private fun ConditionsTextView( + rhId: Long? +) { + val conditionsData = remember { mutableStateOf?>(null) } + val failedToLoad = remember { mutableStateOf(false) } + val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + scope.launch { + try { + val conditions = getUsageConditions(rh = rhId) + + if (conditions != null) { + conditionsData.value = conditions + } else { + failedToLoad.value = true + } + } catch (ex: Exception) { + failedToLoad.value = true + } + } + } + val conditions = conditionsData.value + + if (conditions != null) { + val (usageConditions, conditionsText, _) = conditions + + if (conditionsText != null) { + Box( + modifier = Modifier + .background( + color = MaterialTheme.colors.secondaryVariant, + shape = RoundedCornerShape(12.dp) + ) + .padding(8.dp) + ) { + ColumnWithScrollBar { + Text( + text = conditionsText.trim(), + modifier = Modifier.padding(16.dp) + ) + } + } + } else { + val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md" + ConditionsLinkView(conditionsLink) + } + } else if (failedToLoad.value) { + ConditionsLinkView(defaultConditionsLink) + } else { + DefaultProgressView(null) + } +} + +@Composable +private fun ConditionsLinkView(conditionsLink: String) { + + SectionItemView { + val uriHandler = LocalUriHandler.current + Text(stringResource(MR.strings.operator_conditions_failed_to_load), color = MaterialTheme.colors.onBackground) + Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(conditionsLink) }) + } } \ No newline at end of file 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 0d6f6e2948..e18fa06c2c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1674,11 +1674,17 @@ Operator + %s servers Operator information Description Website Conditions accepted on: %s. Conditions will be considered accepted after: %s. + Operator + Use operator + Use operator %s + Current conditions text couldn\'t be loaded, you can review conditions via this link: + TCP connection