conditions modal in settings

This commit is contained in:
Diogo
2024-11-19 23:43:22 +00:00
parent ff714bbc5d
commit 3dc0c0b69c
4 changed files with 236 additions and 50 deletions
@@ -1034,7 +1034,7 @@ object ChatController {
}
}
suspend fun getUsageConditions(rh: Long?): Triple<UsageConditionsDetail, String?, CR.UsageConditions?>? {
suspend fun getUsageConditions(rh: Long?): Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>? {
val r = sendCmd(rh, CC.ApiGetUsageConditions())
return when (r) {
is CR.UsageConditions -> Triple(r.usageConditions, r.conditionsText, r.acceptedConditions)
@@ -1056,11 +1056,15 @@ object ChatController {
}
}
suspend fun acceptConditions(rh: Long?, conditionsId: Long, operatorIds: List<Long>): CR.ServerOperatorConditions? {
suspend fun acceptConditions(rh: Long?, conditionsId: Long, operatorIds: List<Long>): ServerOperatorConditionsDetail? {
val r = sendCmd(rh, CC.ApiAcceptConditions(conditionsId, operatorIds))
return when (r) {
is CR.ServerOperatorConditions -> r
is CR.ServerOperatorConditions -> r.conditions
else -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.error_accepting_operator_conditions),
"${r.responseType}: ${r.details}"
)
Log.e(TAG, "acceptConditions bad response: ${r.responseType} ${r.details}")
null
}
@@ -3705,7 +3709,7 @@ data class ServerOperator(
val tradeName: String,
val legalName: String?,
val serverDomains: List<String>,
val conditionsAcceptance: ConditionsAcceptance,
var conditionsAcceptance: ConditionsAcceptance,
var enabled: Boolean,
val smpRoles: ServerRoles,
val xftpRoles: ServerRoles,
@@ -5336,7 +5340,7 @@ sealed class CR {
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
@Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List<UserOperatorServers>): CR()
@Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List<UserServersError>): 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: UsageConditionsDetail?): 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()
@@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.acceptConditions
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.getUserServers
import chat.simplex.common.model.ChatController.setUserServers
@@ -44,6 +45,8 @@ fun NetworkAndServersView(close: () -> Unit) {
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val currUserServers = remember { mutableStateOf(emptyList<UserOperatorServers>()) }
val userServers = remember { mutableStateOf(emptyList<UserOperatorServers>()) }
val serverErrors = remember { mutableStateOf(emptyList<UserServersError>()) }
val scope = rememberCoroutineScope()
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
@@ -65,6 +68,7 @@ fun NetworkAndServersView(close: () -> Unit) {
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
currUserServers = currUserServers,
userServers = userServers,
serverErrors = serverErrors,
toggleSocksProxy = { enable ->
val def = NetCfg.defaults
val proxyDef = NetCfg.proxyDefaults
@@ -131,6 +135,7 @@ fun NetworkAndServersView(close: () -> Unit) {
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
currUserServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
userServers: MutableState<List<UserOperatorServers>>,
toggleSocksProxy: (Boolean) -> Unit,
) {
@@ -160,7 +165,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, currentRemoteHost?.remoteHostId) }
srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, currentRemoteHost?.remoteHostId) }
}
}
if (conditionsAction != null && anyOperatorEnabled.value) {
@@ -610,9 +615,10 @@ private fun ServerOperatorRow(
operator: ServerOperator,
currUserServers: MutableState<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
rhId: Long?
) {
SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorView(currUserServers, userServers, index, rhId) } }) {
SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorView(currUserServers, userServers, serverErrors, index, rhId) } }) {
Image(
painterResource(MR.images.decentralized),
operator.tradeName,
@@ -713,6 +719,28 @@ fun showUpdateNetworkSettingsDialog(
)
}
fun updateOperatorsConditionsAcceptance(usvs: MutableState<List<UserOperatorServers>>, updatedOperators: List<ServerOperator>) {
for (i in usvs.value.indices) {
val updatedOperator = updatedOperators.firstOrNull { it.operatorId == usvs.value[i].operator?.operatorId } ?: continue
usvs.value[i].operator?.conditionsAcceptance = updatedOperator.conditionsAcceptance
}
}
suspend fun validateServers(
rhId: Long?,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>
) {
val userServersToValidate = userServers.value
try {
val errors = chatController.validateServers(rhId, userServersToValidate) ?: return
serverErrors.value = errors
} catch (ex: Exception) {
Log.e(TAG, ex.stackTraceToString())
}
}
@Preview
@Composable
fun PreviewNetworkAndServersLayout() {
@@ -723,7 +751,8 @@ fun PreviewNetworkAndServersLayout() {
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
toggleSocksProxy = {},
currUserServers = remember { mutableStateOf(emptyList()) },
userServers = remember { mutableStateOf(emptyList()) }
userServers = remember { mutableStateOf(emptyList()) },
serverErrors = remember { mutableStateOf(emptyList()) }
)
}
}
@@ -7,36 +7,46 @@ import SectionView
import TextIconSpaced
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.platform.*
import chat.simplex.common.ui.theme.*
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 ModalData.OperatorView(currUserServers: MutableState<List<UserOperatorServers>>, userServers: MutableState<List<UserOperatorServers>>, operatorIndex: Int, rhId: Long?) {
fun ModalData.OperatorView(
currUserServers: MutableState<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
operatorIndex: Int,
rhId: Long?
) {
val testing = remember { mutableStateOf(false) }
val operator = remember { userServers.value[operatorIndex].operator_ }
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)
OperatorViewLayout(currUserServers, userServers, serverErrors, operatorIndex, rhId)
if (testing.value) {
DefaultProgressView(null)
}
@@ -44,7 +54,13 @@ fun ModalData.OperatorView(currUserServers: MutableState<List<UserOperatorServer
}
@Composable
fun OperatorViewLayout(currUserServers: MutableState<List<UserOperatorServers>>, userServers: MutableState<List<UserOperatorServers>>, operatorIndex: Int, rhId: Long?) {
fun OperatorViewLayout(
currUserServers: MutableState<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
operatorIndex: Int,
rhId: Long?
) {
val operator = remember { userServers.value[operatorIndex].operator_ }
Column {
@@ -61,7 +77,7 @@ fun OperatorViewLayout(currUserServers: MutableState<List<UserOperatorServers>>,
TextIconSpaced()
Text(operator.tradeName, color = MaterialTheme.colors.onBackground)
}
UseOperatorToggle(currUserServers = currUserServers, userServers = userServers, operatorIndex = operatorIndex, rhId = rhId)
UseOperatorToggle(currUserServers = currUserServers, userServers = userServers, serverErrors = serverErrors, operatorIndex = operatorIndex, rhId = rhId)
}
if (operator.enabled) {
if (userServers.value[operatorIndex].smpServers.filter { !it.deleted }.isNotEmpty()) {
@@ -96,6 +112,7 @@ private fun OperatorInfoView(serverOperator: ServerOperator) {
private fun UseOperatorToggle(
currUserServers: MutableState<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
operatorIndex: Int,
rhId: Long?
) {
@@ -111,22 +128,21 @@ private fun UseOperatorToggle(
checked = operator.enabled,
onCheckedChange = { enabled ->
if (enabled) {
when (operator.conditionsAcceptance) {
when (val conditionsAcceptance = 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 { _ ->
if (conditionsAcceptance.deadline == null) {
ModalManager.start.showModalCloseable { close ->
SingleOperatorUsageConditionsView(
currUserServers = currUserServers,
userServers = userServers,
serverErrors = serverErrors,
operatorIndex = operatorIndex,
rhId = rhId
rhId = rhId,
close = close
)
}
} else {
@@ -146,31 +162,140 @@ private fun UseOperatorToggle(
private fun SingleOperatorUsageConditionsView(
currUserServers: MutableState<List<UserOperatorServers>>,
userServers: MutableState<List<UserOperatorServers>>,
serverErrors: MutableState<List<UserServersError>>,
operatorIndex: Int,
rhId: Long?
rhId: Long?,
close: () -> Unit
) {
val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } }
val operator = remember { userServers.value[operatorIndex].operator_ }
val scope = rememberCoroutineScope()
ColumnWithScrollBar {
AppBarTitle(String.format(stringResource(MR.strings.use_operator_x), operator.tradeName))
suspend fun acceptForOperators(rhId: Long?, operatorIds: List<Long>, operatorIndexToEnable: Int, close: () -> Unit) {
try {
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return
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 {
//
chatModel.conditions.value = r
updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators)
updateOperatorsConditionsAcceptance(userServers, r.serverOperators)
userServers.value.getOrNull(operatorIndexToEnable)?.operator?.enabled = true
validateServers(rhId, userServers, serverErrors)
close()
} catch (ex: Exception) {
Log.e(TAG, ex.stackTraceToString())
}
}
@Composable
fun AcceptConditionsButton(close: () -> Unit) {
// Opened operator or Other enabled operators with conditions not accepted
val operatorIds = chatModel.conditions.value.serverOperators
.filter { it.operatorId == operator.id || (it.enabled && !it.conditionsAcceptance.conditionsAccepted) }
.map { it.operatorId }
Column(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) {
Button(
onClick = {
scope.launch {
acceptForOperators(rhId = rhId, operatorIds = operatorIds, operatorIndexToEnable = operatorIndex, close)
}
},
shape = CircleShape,
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 2, vertical = DEFAULT_PADDING),
colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary)
) {
Text(stringResource(MR.strings.accept_conditions), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)
}
}
}
@Composable
fun UsageConditionsDestinationView(close: () -> Unit) {
ColumnWithScrollBar(modifier = Modifier.fillMaxSize()) {
NonScrollableTitle(stringResource(MR.strings.operator_conditions_of_use))
Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_PADDING)) {
ConditionsTextView(rhId)
}
AcceptConditionsButton(close)
}
}
@Composable
fun UsageConditionsNavLinkButton() {
Text(
stringResource(MR.strings.view_conditions),
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable {
ModalManager.start.showModalCloseable { close ->
UsageConditionsDestinationView(close)
}
}
)
}
ColumnWithScrollBar(modifier = Modifier.fillMaxSize()) {
NonScrollableTitle(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
Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) {
ConditionsTextView(rhId)
}
} else if (operatorsWithConditionsAccepted.isNotEmpty()) {
SectionItemView {
Text(String.format(stringResource(MR.strings.operator_conditions_accepted_for_some), operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }))
}
SectionItemView {
Text(String.format(stringResource(MR.strings.operator_same_conditions_will_be_applied), operator.legalName_))
}
ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex)
SectionItemView {
UsageConditionsNavLinkButton()
}
Spacer(Modifier.fillMaxWidth().weight(1f))
AcceptConditionsButton(close)
} else {
SectionItemView {
Text(String.format(stringResource(MR.strings.operator_in_order_to_use_accept_conditions, operator.legalName_)))
}
SectionItemView {
ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex)
}
Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) {
ConditionsTextView(rhId)
}
AcceptConditionsButton(close)
}
}
}
@Composable
private fun NonScrollableTitle(title: String) {
val titleColor = MaterialTheme.appColors.title
val theme = CurrentColors.collectAsState()
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
else // color is not updated when changing themes if I pass null here
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Text(
title,
Modifier
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Start
)
}
@Composable
private fun ConditionsTextView(
rhId: Long?
) {
val conditionsData = remember { mutableStateOf<Triple<UsageConditionsDetail, String?, CR.UsageConditions?>?>(null) }
val conditionsData = remember { mutableStateOf<Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>?>(null) }
val failedToLoad = remember { mutableStateOf(false) }
val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
val scope = rememberCoroutineScope()
@@ -196,21 +321,22 @@ private fun ConditionsTextView(
val (usageConditions, conditionsText, _) = conditions
if (conditionsText != null) {
Box(
modifier = Modifier
.background(
color = MaterialTheme.colors.secondaryVariant,
shape = RoundedCornerShape(12.dp)
)
.padding(8.dp)
) {
ColumnWithScrollBar {
val scrollState = rememberScrollState()
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(12.dp))
.verticalScroll(scrollState)
.background(
color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f),
)
.padding(8.dp)
) {
Text(
text = conditionsText.trim(),
text = conditionsText.trimIndent(),
modifier = Modifier.padding(16.dp)
)
}
}
} else {
val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md"
ConditionsLinkView(conditionsLink)
@@ -224,10 +350,30 @@ private fun ConditionsTextView(
@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) })
}
}
}
@Composable
private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperatorServers>, operatorIndex: Int) {
val otherOperatorsToApply = remember {
derivedStateOf {
chatModel.conditions.value.serverOperators.filter {
it.enabled &&
!it.conditionsAcceptance.conditionsAccepted &&
it.operatorId != userServers[operatorIndex].operator_.operatorId
}
}
}
if (otherOperatorsToApply.value.isNotEmpty()) {
SectionItemView {
Text(
String.format(stringResource(MR.strings.operators_conditions_will_also_apply), otherOperatorsToApply.value.joinToString(", ") { it.legalName_ }),
)
}
}
}
@@ -117,6 +117,7 @@
<string name="no_media_servers_configured_for_sending">No media &amp; file servers configured for sending.</string>
<string name="no_media_servers_configured_for_private_routing">No media &amp; file servers configured for private routing.</string>
<string name="for_chat_profile">For chat profile %s:</string>
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Connection timeout</string>
@@ -1692,7 +1693,13 @@
<string name="operator_use_operator_toggle_description">Use operator</string>
<string name="use_operator_x">Use operator %s</string>
<string name="operator_conditions_failed_to_load">Current conditions text couldn\'t be loaded, you can review conditions via this link:</string>
<string name="operator_conditions_accepted_for_some">Conditions are already accepted for following operator(s): %s.</string>
<string name="operator_same_conditions_will_be_applied">Same conditions will apply to operator %s.</string>
<string name="operators_conditions_will_also_apply">Conditions will also apply for following operator(s) you use: %s.</string>
<string name="view_conditions">View conditions</string>
<string name="accept_conditions">Accept conditions</string>
<string name="operator_conditions_of_use">Conditions of use</string>
<string name="operator_in_order_to_use_accept_conditions">In order to use operator %s, accept conditions of use.</string>
<!-- AdvancedNetworkSettings.kt -->
<string name="network_option_tcp_connection">TCP connection</string>