mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-10 05:41:56 +00:00
ChooseServerOperators
This commit is contained in:
@@ -15,7 +15,6 @@ import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -42,7 +41,6 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
@@ -194,6 +192,13 @@ fun MainScreen() {
|
||||
OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
|
||||
OnboardingStage.LinkAMobile -> LinkAMobile()
|
||||
OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
|
||||
OnboardingStage.Step3_ChooseServerOperators -> {
|
||||
val modalData = remember { ModalData() }
|
||||
modalData.ChooseServerOperators(true) {}
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
}
|
||||
// Ensure backwards compatibility with old onboarding stage for address creation, otherwise notification setup would be skipped
|
||||
OnboardingStage.Step3_CreateSimpleXAddress -> SetNotificationsMode(chatModel)
|
||||
OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
|
||||
+32
-28
@@ -31,6 +31,7 @@ import com.charleskorn.kaml.Yaml
|
||||
import com.charleskorn.kaml.YamlConfiguration
|
||||
import chat.simplex.res.MR
|
||||
import com.russhwolf.settings.Settings
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -3604,48 +3605,51 @@ enum class OperatorTag {
|
||||
@SerialName("demo") Demo
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerOperatorInfo(
|
||||
val description: String,
|
||||
val website: String,
|
||||
val logo: String,
|
||||
val largeLogo: String,
|
||||
val logoDarkMode: String,
|
||||
val largeLogoDarkMode: String
|
||||
val logo: ImageResource,
|
||||
val largeLogo: ImageResource,
|
||||
val logoDarkMode: ImageResource,
|
||||
val largeLogoDarkMode: ImageResource
|
||||
)
|
||||
// TODO: Check logos
|
||||
val operatorsInfo: Map<OperatorTag, ServerOperatorInfo> = mapOf(
|
||||
OperatorTag.SimpleX to ServerOperatorInfo(
|
||||
description = "SimpleX Chat preset servers",
|
||||
website = "https://simplex.chat",
|
||||
logo = "decentralized",
|
||||
largeLogo = "logo",
|
||||
logoDarkMode = "decentralized-light",
|
||||
largeLogoDarkMode = "logo-light"
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
),
|
||||
OperatorTag.Flux to ServerOperatorInfo(
|
||||
description = "Flux is the largest decentralized cloud infrastructure, leveraging a global network of user-operated computational nodes. Flux offers a powerful, scalable, and affordable platform designed to support individuals, businesses, and cutting-edge technologies like AI. With high uptime and worldwide distribution, Flux ensures reliable, accessible cloud computing for all.",
|
||||
website = "https://runonflux.com",
|
||||
logo = "flux_logo_symbol",
|
||||
largeLogo = "flux_logo",
|
||||
logoDarkMode = "flux_logo_symbol",
|
||||
largeLogoDarkMode = "flux_logo-light"
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
// LALAL
|
||||
// logo = "flux_logo_symbol",
|
||||
// largeLogo = "flux_logo",
|
||||
// logoDarkMode = "flux_logo_symbol",
|
||||
// largeLogoDarkMode = "flux_logo-light"
|
||||
),
|
||||
OperatorTag.XYZ to ServerOperatorInfo(
|
||||
description = "XYZ servers",
|
||||
website = "XYZ website",
|
||||
logo = "shield",
|
||||
largeLogo = "logo",
|
||||
logoDarkMode = "shield",
|
||||
largeLogoDarkMode = "logo-light"
|
||||
logo = MR.images.shield,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.shield,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
),
|
||||
OperatorTag.Demo to ServerOperatorInfo(
|
||||
description = "Demo operator",
|
||||
website = "Demo website",
|
||||
logo = "decentralized",
|
||||
largeLogo = "logo",
|
||||
logoDarkMode = "decentralized-light",
|
||||
largeLogoDarkMode = "logo-light"
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
)
|
||||
)
|
||||
|
||||
@@ -3727,10 +3731,10 @@ data class ServerOperator(
|
||||
val dummyOperatorInfo = ServerOperatorInfo(
|
||||
description = "Default",
|
||||
website = "Default",
|
||||
logo = "decentralized",
|
||||
largeLogo = "logo",
|
||||
logoDarkMode = "decentralized-light",
|
||||
largeLogoDarkMode = "logo-light"
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
)
|
||||
|
||||
val sampleData1 = ServerOperator(
|
||||
@@ -3810,11 +3814,11 @@ data class ServerOperator(
|
||||
}
|
||||
}
|
||||
|
||||
val logo: String get() {
|
||||
val logo: ImageResource get() {
|
||||
return if (CurrentColors.value.base == DefaultTheme.LIGHT) info.logo else info.logoDarkMode
|
||||
}
|
||||
|
||||
val largeLogo: String get() {
|
||||
val largeLogo: ImageResource get() {
|
||||
return if (CurrentColors.value.base == DefaultTheme.LIGHT) info.largeLogo else info.largeLogoDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -138,13 +138,12 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
}
|
||||
} else if (startChat().await()) {
|
||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||
val next = if (appPlatform.isAndroid) {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
} else {
|
||||
OnboardingStage.OnboardingComplete
|
||||
}
|
||||
val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
next
|
||||
if (appPlatform.isAndroid) {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
} else {
|
||||
OnboardingStage.OnboardingComplete
|
||||
}
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
}
|
||||
|
||||
+2
-2
@@ -165,7 +165,7 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) {
|
||||
if (!chatModel.connectedToRemote()) {
|
||||
chatModel.localUserCreated.value = true
|
||||
}
|
||||
controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators)
|
||||
controller.startChat(user)
|
||||
controller.switchUIRemoteHost(null)
|
||||
close()
|
||||
@@ -204,7 +204,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: ()
|
||||
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) {
|
||||
OnboardingStage.Step2_5_SetupDatabasePassphrase
|
||||
} else {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
OnboardingStage.Step3_ChooseServerOperators
|
||||
})
|
||||
} else {
|
||||
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
|
||||
|
||||
+332
-5
@@ -1,9 +1,336 @@
|
||||
package chat.simplex.common.views.onboarding
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import SectionBottomSpacer
|
||||
import SectionTextFooter
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ServerOperator
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.changeNotificationsMode
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsTextView
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ChooseServerOperators(onboarding: Boolean) {
|
||||
Column { }
|
||||
}
|
||||
fun ModalData.ChooseServerOperators(onboarding: Boolean, close: () -> Unit) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({}, showClose = false, endButtons = {
|
||||
IconButton({ (if (onboarding) ModalManager.fullscreen else ModalManager.start).showModal { ChooseServerOperatorsInfoView() } }) {
|
||||
Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}) {
|
||||
val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } }
|
||||
val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } }
|
||||
val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer),
|
||||
maxIntrinsicSize = true
|
||||
) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(MR.strings.onboarding_choose_server_operators))
|
||||
}
|
||||
Column((
|
||||
if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier)
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_select_network_operators_to_use))
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
Column((
|
||||
if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
serverOperators.value.forEachIndexed { index, srvOperator ->
|
||||
OperatorCheckView(srvOperator, selectedOperatorIds)
|
||||
if (index != serverOperators.value.lastIndex) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_configure_via_settings), textAlign = TextAlign.Center)
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
val reviewForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted }
|
||||
val canReviewLater = reviewForOperators.all { it.conditionsAcceptance.usageAllowed }
|
||||
val currEnabledOperatorIds = serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet()
|
||||
|
||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
val enabled = selectedOperatorIds.value.isNotEmpty()
|
||||
when {
|
||||
reviewForOperators.isNotEmpty() -> ReviewConditionsButton(enabled, onboarding, selectedOperators, selectedOperatorIds)
|
||||
selectedOperatorIds.value != currEnabledOperatorIds && enabled -> SetOperatorsButton(true, onboarding, serverOperators, selectedOperatorIds, close)
|
||||
else -> ContinueButton(enabled, onboarding, close)
|
||||
}
|
||||
if (onboarding && reviewForOperators.isEmpty()) {
|
||||
TextButtonBelowOnboardingButton(stringResource(MR.strings.operator_conditions_of_use)) {
|
||||
ModalManager.fullscreen.showModalCloseable { close ->
|
||||
UsageConditionsView(
|
||||
currUserServers = listOf(),
|
||||
userServers = listOf(),
|
||||
close = close
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (onboarding || reviewForOperators.isEmpty()) {
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
if (!onboarding && reviewForOperators.isNotEmpty()) {
|
||||
ReviewLaterButton(canReviewLater, close)
|
||||
SectionTextFooter(
|
||||
annotatedStringResource(MR.strings.onboarding_network_operators_conditions_will_be_accepted) +
|
||||
AnnotatedString(" ") +
|
||||
annotatedStringResource(MR.strings.onboarding_network_operators_conditions_you_can_configure),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OperatorCheckView(serverOperator: ServerOperator, selectedOperatorIds: MutableState<Set<Long>>) {
|
||||
val checked = selectedOperatorIds.value.contains(serverOperator.operatorId)
|
||||
TextButton({
|
||||
if (checked) {
|
||||
selectedOperatorIds.value -= serverOperator.operatorId
|
||||
} else {
|
||||
selectedOperatorIds.value += serverOperator.operatorId
|
||||
}
|
||||
},
|
||||
border = BorderStroke(1.dp, color = if (checked) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) {
|
||||
Row(Modifier.padding(DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp))
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF).weight(1f))
|
||||
CircleCheckbox(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CircleCheckbox(checked: Boolean) {
|
||||
if (checked) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_circle_filled),
|
||||
null,
|
||||
Modifier.size(26.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_check_filled),
|
||||
null,
|
||||
Modifier.size(20.dp), tint = MaterialTheme.colors.background
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_circle),
|
||||
null,
|
||||
Modifier.size(26.dp),
|
||||
tint = MaterialTheme.colors.secondary.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewConditionsButton(
|
||||
enabled: Boolean,
|
||||
onboarding: Boolean,
|
||||
selectedOperators: State<List<ServerOperator>>,
|
||||
selectedOperatorIds: State<Set<Long>>
|
||||
) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.operator_review_conditions,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = {
|
||||
ModalManager.fullscreen.showModalCloseable { close ->
|
||||
ReviewConditionsView(onboarding, selectedOperators, selectedOperatorIds, close)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State<List<ServerOperator>>, selectedOperatorIds: State<Set<Long>>, close: () -> Unit) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.onboarding_network_operators_update,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = {
|
||||
withBGApi {
|
||||
val enabledOperators = enabledOperators(serverOperators.value, selectedOperatorIds.value)
|
||||
if (enabledOperators != null) {
|
||||
val r = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators)
|
||||
if (r != null) {
|
||||
chatModel.conditions.value = r
|
||||
}
|
||||
continueToNextStep(onboarding, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.onboarding_network_operators_continue,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = {
|
||||
continueToNextStep(onboarding, close)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewLaterButton(enabled: Boolean, close: () -> Unit) {
|
||||
TextButtonBelowOnboardingButton(
|
||||
stringResource(MR.strings.onboarding_network_operators_review_later),
|
||||
onClick = if (!enabled) null else {{ continueToNextStep(false, close) }}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewConditionsView(
|
||||
onboarding: Boolean,
|
||||
selectedOperators: State<List<ServerOperator>>,
|
||||
selectedOperatorIds: State<Set<Long>>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val operatorsWithConditionsAccepted = chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted }
|
||||
val acceptForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted }
|
||||
ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false)
|
||||
if (operatorsWithConditionsAccepted.isNotEmpty()) {
|
||||
ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ })
|
||||
ReadableText(MR.strings.operator_same_conditions_will_apply_to_operators, args = acceptForOperators.joinToString(", ") { it.legalName_ })
|
||||
} else {
|
||||
ReadableText(MR.strings.operator_conditions_will_be_accepted_for_some, args = acceptForOperators.joinToString(", ") { it.legalName_ })
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
ConditionsTextView(chatModel.remoteHostId())
|
||||
}
|
||||
Column(Modifier.padding(top = DEFAULT_PADDING * 2).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
AcceptConditionsButton(onboarding, selectedOperators, selectedOperatorIds, close)
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AcceptConditionsButton(
|
||||
onboarding: Boolean,
|
||||
selectedOperators: State<List<ServerOperator>>,
|
||||
selectedOperatorIds: State<Set<Long>>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.accept_conditions,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
withBGApi {
|
||||
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
|
||||
val acceptForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted }
|
||||
val operatorIds = acceptForOperators.map { it.operatorId }
|
||||
val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = operatorIds)
|
||||
if (r != null) {
|
||||
chatModel.conditions.value = r
|
||||
val enabledOperators = enabledOperators(r.serverOperators, selectedOperatorIds.value)
|
||||
if (enabledOperators != null) {
|
||||
val r2 = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators)
|
||||
if (r2 != null) {
|
||||
chatModel.conditions.value = r2
|
||||
if (onboarding) { close() }
|
||||
continueToNextStep(onboarding, close)
|
||||
}
|
||||
} else {
|
||||
if (onboarding) { close() }
|
||||
continueToNextStep(onboarding, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun continueToNextStep(onboarding: Boolean, close: () -> Unit) {
|
||||
if (onboarding) {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun enabledOperators(operators: List<ServerOperator>, selectedOperatorIds: Set<Long>): List<ServerOperator>? {
|
||||
val ops = ArrayList(operators)
|
||||
if (ops.isNotEmpty()) {
|
||||
for (i in ops.indices) {
|
||||
val op = ops[i]
|
||||
ops[i] = op.copy(enabled = selectedOperatorIds.contains(op.operatorId))
|
||||
}
|
||||
val haveSMPStorage = ops.any { it.enabled && it.smpRoles.storage }
|
||||
val haveSMPProxy = ops.any { it.enabled && it.smpRoles.proxy }
|
||||
val haveXFTPStorage = ops.any { it.enabled && it.xftpRoles.storage }
|
||||
val haveXFTPProxy = ops.any { it.enabled && it.xftpRoles.proxy }
|
||||
val firstEnabledIndex = ops.indexOfFirst { it.enabled }
|
||||
if (haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy) {
|
||||
return ops
|
||||
} else if (firstEnabledIndex != -1) {
|
||||
var op = ops[firstEnabledIndex]
|
||||
if (!haveSMPStorage) op = op.copy(smpRoles = op.smpRoles.copy(storage = true))
|
||||
if (!haveSMPProxy) op = op.copy(smpRoles = op.smpRoles.copy(proxy = true))
|
||||
if (!haveXFTPStorage) op = op.copy(xftpRoles = op.xftpRoles.copy(storage = true))
|
||||
if (!haveXFTPProxy) op = op.copy(xftpRoles = op.xftpRoles.copy(proxy = true))
|
||||
ops[firstEnabledIndex] = op
|
||||
return ops
|
||||
} else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChooseServerOperatorsInfoView() {
|
||||
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.onboarding_network_operators), withPadding = false)
|
||||
ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators))
|
||||
ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing))
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ enum class OnboardingStage {
|
||||
Step2_CreateProfile,
|
||||
LinkAMobile,
|
||||
Step2_5_SetupDatabasePassphrase,
|
||||
Step3_ChooseServerOperators,
|
||||
Step3_CreateSimpleXAddress,
|
||||
Step4_SetNotificationsMode,
|
||||
OnboardingComplete
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.*
|
||||
@@ -36,7 +35,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||
fun nextStep() {
|
||||
if (appPlatform.isAndroid || chatModel.currentUser.value != null) {
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators)
|
||||
} else {
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile)
|
||||
}
|
||||
|
||||
+3
-2
@@ -164,14 +164,15 @@ fun OnboardingActionButton(
|
||||
@Composable
|
||||
fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) {
|
||||
val state = getKeyboardState()
|
||||
val enabled = onClick != null
|
||||
val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING)
|
||||
val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING * 2)
|
||||
if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) {
|
||||
TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = onClick != null) {
|
||||
TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = enabled) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp),
|
||||
color = MaterialTheme.colors.primary,
|
||||
color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
+3
-4
@@ -40,13 +40,12 @@ fun WhatsNewView(showWhatsNew: MutableState<Boolean> = mutableStateOf(true), sho
|
||||
Log.d(TAG, "WhatsNewView setConditionsNotified error: ${e.message}")
|
||||
}
|
||||
}
|
||||
ModalManager.fullscreen.showModalCloseable(showClose = false) { close ->
|
||||
ChooseServerOperators(onboarding = false, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showOperatorsNotice) {
|
||||
return ChooseServerOperators(onboarding = false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun featureDescription(icon: ImageResource?, titleId: StringResource, descrId: StringResource?, link: String?, subfeatures: List<Pair<ImageResource, StringResource>>) {
|
||||
@Composable
|
||||
|
||||
+12
-9
@@ -673,6 +673,13 @@ private fun ModalData.UsageConditionsView(conditionsAction: UsageConditionsActio
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.UsageConditionsView(currUserServers: List<UserOperatorServers>, userServers: List<UserOperatorServers>, close: () -> Unit) {
|
||||
ModalView(close = close) {
|
||||
Text("Hello")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConditionsButton(conditionsAction: UsageConditionsAction) {
|
||||
SectionItemView(
|
||||
@@ -737,16 +744,12 @@ 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 = usvs.value.toMutableList().apply {
|
||||
this[i] = this[i].copy(
|
||||
operator = this[i].operator?.copy(
|
||||
conditionsAcceptance = updatedOperator.conditionsAcceptance
|
||||
)
|
||||
)
|
||||
}
|
||||
val modified = ArrayList(usvs.value)
|
||||
for (i in modified.indices) {
|
||||
val updatedOperator = updatedOperators.firstOrNull { it.operatorId == modified[i].operator?.operatorId } ?: continue
|
||||
modified[i] = modified[i].copy(operator = modified[i].operator?.copy(conditionsAcceptance = updatedOperator.conditionsAcceptance))
|
||||
}
|
||||
usvs.value = modified
|
||||
}
|
||||
|
||||
suspend fun validateServers(
|
||||
|
||||
@@ -1048,6 +1048,19 @@
|
||||
<string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string>
|
||||
<string name="use_random_passphrase">Use random passphrase</string>
|
||||
|
||||
<!-- ChooseServerOperators.kt -->
|
||||
<string name="onboarding_choose_server_operators">Choose operators</string>
|
||||
<string name="onboarding_network_operators">Network operators</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">When more than one network operator is enabled, the app will use the servers of different operators for each conversation.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Select network operators to use.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">You can configure servers via settings.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Conditions will be accepted for enabled operators after 30 days.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">You can configure operators in Network & servers settings.</string>
|
||||
<string name="onboarding_network_operators_review_later">Review later</string>
|
||||
<string name="onboarding_network_operators_update">Update</string>
|
||||
<string name="onboarding_network_operators_continue">Continue</string>
|
||||
|
||||
<!-- Call -->
|
||||
<string name="incoming_video_call">Incoming video call</string>
|
||||
<string name="incoming_audio_call">Incoming audio call</string>
|
||||
@@ -1697,8 +1710,13 @@
|
||||
<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"><![CDATA[Conditions are already accepted for following operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Same conditions will apply to operator <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Same conditions will apply to operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Conditions are already accepted for following operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_applied"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Same conditions will apply to operator: <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Conditions will be accepted for operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operators_conditions_will_also_apply"><![CDATA[Conditions will also apply for following operator(s) you use: <b>%s</b>.]]></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>
|
||||
|
||||
Reference in New Issue
Block a user