desktop: onboarding improvements (#5294)

* consistent space to bottom on future of messaging

* consistent button suze on server operators

* updated setup database passphrase screen

* ability to cancel random passphrase

* reduce conditions padding to header

* show scrollbar in desktop

* EOLs

* EOL

* fix random passphrase param when deleting database and recreating new one

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
Diogo
2024-12-02 21:00:55 +00:00
committed by GitHub
parent 92967dfe0c
commit c04e952620
8 changed files with 141 additions and 69 deletions
@@ -0,0 +1,19 @@
package chat.simplex.common.views.usersettings.networkAndServers
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
actual fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)){
Box(
modifier = modifier
.verticalScroll(scrollState)
.padding(8.dp)
) {
content()
}
}
@@ -525,6 +525,7 @@ fun deleteChatDatabaseFilesAndState() {
wallpapersDir.mkdirs()
DatabaseUtils.ksDatabasePassword.remove()
appPrefs.newDatabaseInitialized.set(false)
chatModel.desktopOnboardingRandomPassword.value = false
controller.appPrefs.storeDBPassphrase.set(true)
controller.ctrl = null
@@ -169,7 +169,7 @@ private fun ReviewConditionsButton(
modalManager: ModalManager
) {
OnboardingActionButton(
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.operator_review_conditions,
onboarding = null,
enabled = enabled,
@@ -184,7 +184,7 @@ private fun ReviewConditionsButton(
@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,
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.onboarding_network_operators_update,
onboarding = null,
enabled = enabled,
@@ -206,7 +206,7 @@ private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOper
@Composable
private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) {
OnboardingActionButton(
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.onboarding_network_operators_continue,
onboarding = null,
enabled = enabled,
@@ -235,7 +235,7 @@ private fun ReviewConditionsView(
val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } }
val acceptForOperators = remember { 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)
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false, bottomPadding = DEFAULT_PADDING)
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_ })
@@ -1,6 +1,7 @@
package chat.simplex.common.views.onboarding
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
@@ -23,7 +24,7 @@ import dev.icerock.moko.resources.StringResource
@Composable
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) {
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false)
ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues)
ReadableText(MR.strings.only_client_devices_store_contacts_groups_e2e_encrypted_messages)
@@ -35,11 +36,12 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
Spacer(Modifier.fillMaxHeight().weight(1f))
if (onboardingStage != null) {
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() })
// Reserve space
TextButtonBelowOnboardingButton("", null)
}
}
Spacer(Modifier.height(DEFAULT_PADDING))
}
}
@@ -1,6 +1,5 @@
package chat.simplex.common.views.onboarding
import SectionTextFooter
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.*
@@ -12,7 +11,6 @@ import androidx.compose.ui.focus.*
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -106,14 +104,31 @@ private fun SetupDatabasePassphraseLayout(
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
ModalView({}, showClose = false) {
ColumnWithScrollBar(
Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2),
Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarTitle(stringResource(MR.strings.setup_database_passphrase))
Spacer(Modifier.weight(1f))
val onClickUpdate = {
// Don't do things concurrently. Shouldn't be here concurrently, just in case
if (!progressIndicator.value) {
encryptDatabaseAlert(onConfirmEncrypt)
}
}
val disabled = currentKey.value == newKey.value ||
newKey.value != confirmNewKey.value ||
newKey.value.isEmpty() ||
!validKey(currentKey.value) ||
!validKey(newKey.value) ||
progressIndicator.value
Column(Modifier.width(600.dp), horizontalAlignment = Alignment.CenterHorizontally) {
val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)
ReadableText(MR.strings.you_have_to_enter_passphrase_every_time, TextAlign.Center, padding = PaddingValues(), style = textStyle )
Spacer(Modifier.height(DEFAULT_PADDING))
ReadableText(MR.strings.impossible_to_recover_passphrase, TextAlign.Center, padding = PaddingValues(), style = textStyle)
Spacer(Modifier.height(DEFAULT_PADDING))
Column(Modifier.width(600.dp)) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
@@ -138,18 +153,6 @@ private fun SetupDatabasePassphraseLayout(
isValid = ::validKey,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
)
val onClickUpdate = {
// Don't do things concurrently. Shouldn't be here concurrently, just in case
if (!progressIndicator.value) {
encryptDatabaseAlert(onConfirmEncrypt)
}
}
val disabled = currentKey.value == newKey.value ||
newKey.value != confirmNewKey.value ||
newKey.value.isEmpty() ||
!validKey(currentKey.value) ||
!validKey(newKey.value) ||
progressIndicator.value
PassphraseField(
confirmNewKey,
@@ -167,21 +170,17 @@ private fun SetupDatabasePassphraseLayout(
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
)
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {
SetPassphraseButton(disabled, onClickUpdate)
}
Column {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
Spacer(Modifier.weight(1f))
SkipButton(progressIndicator.value) {
chatModel.desktopOnboardingRandomPassword.value = true
nextStep()
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp), horizontalAlignment = Alignment.CenterHorizontally) {
SetPassphraseButton(disabled, onClickUpdate)
SkipButton(progressIndicator.value) {
randomPassphraseAlert {
chatModel.desktopOnboardingRandomPassword.value = true
nextStep()
}
}
}
}
}
@@ -190,30 +189,18 @@ private fun SetupDatabasePassphraseLayout(
@Composable
private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) {
SimpleButtonIconEnded(
stringResource(MR.strings.set_database_passphrase),
painterResource(MR.images.ic_check),
style = MaterialTheme.typography.h2,
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
disabled = disabled,
click = onClick
OnboardingActionButton(
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.set_database_passphrase,
onboarding = null,
onclick = onClick,
enabled = !disabled
)
}
@Composable
private fun SkipButton(disabled: Boolean, onClick: () -> Unit) {
SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color =
if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick)
Text(
stringResource(MR.strings.you_can_change_it_later),
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING * 3)
.padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING - 5.dp),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.secondary,
textAlign = TextAlign.Center,
)
TextButtonBelowOnboardingButton(stringResource(MR.strings.use_random_passphrase), onClick = if (disabled) null else onClick)
}
@Composable
@@ -238,3 +225,12 @@ private suspend fun startChat(key: String?) {
m.chatDbChanged.value = false
m.chatRunning.value = true
}
private fun randomPassphraseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.use_random_passphrase),
text = generalGetString(MR.strings.you_can_change_it_later),
confirmText = generalGetString(MR.strings.ok),
onConfirm = onConfirm,
)
}
@@ -734,7 +734,7 @@ fun UsageConditionsView(
}
ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false)
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING)
when (val conditionsAction = chatModel.conditions.value.conditionsAction) {
is UsageConditionsAction.Review -> {
if (conditionsAction.operators.isNotEmpty()) {
@@ -14,6 +14,7 @@ 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.graphics.*
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
@@ -615,7 +616,6 @@ fun ConditionsTextView(
val failedToLoad = remember { mutableStateOf(false) }
val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
val scope = rememberCoroutineScope()
// can show conditions when animation between modals finishes to prevent glitches
val canShowConditionsAt = remember { System.currentTimeMillis() + 300 }
LaunchedEffect(Unit) {
@@ -645,18 +645,18 @@ fun ConditionsTextView(
if (conditionsText != null) {
val scrollState = rememberScrollState()
Box(
modifier = Modifier
.fillMaxSize()
.border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp))
.verticalScroll(scrollState)
.padding(8.dp)
) {
val parentUriHandler = LocalUriHandler.current
CompositionLocalProvider(LocalUriHandler provides remember { internalUriHandler(parentUriHandler) }) {
ConditionsMarkdown(conditionsText)
}
ConditionsBox(
Modifier
.fillMaxSize()
.border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp))
.clip(shape = RoundedCornerShape(12.dp)),
scrollState
) {
val parentUriHandler = LocalUriHandler.current
CompositionLocalProvider(LocalUriHandler provides remember { internalUriHandler(parentUriHandler) }) {
ConditionsMarkdown(conditionsText)
}
}
} else {
val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md"
ConditionsLinkView(conditionsLink)
@@ -668,6 +668,9 @@ fun ConditionsTextView(
}
}
@Composable
expect fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit))
@Composable
private fun ConditionsMarkdown(text: String) {
Markdown(text,
@@ -0,0 +1,51 @@
package chat.simplex.common.views.usersettings.networkAndServers
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.DesktopScrollBar
import chat.simplex.common.views.helpers.detectCursorMove
import kotlinx.coroutines.*
@Composable
actual fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)) {
val scope = rememberCoroutineScope()
val scrollBarAlpha = remember { Animatable(0f) }
val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) }
val scrollBarDraggingState = remember { mutableStateOf(false) }
val scrollModifier = remember {
Modifier
.pointerInput(Unit) {
detectCursorMove {
scope.launch {
scrollBarAlpha.animateTo(1f)
}
scrollJob.value.cancel()
scrollJob.value = scope.launch {
delay(1000L)
scrollBarAlpha.animateTo(0f)
}
}
}
}
Box(modifier = modifier) {
Box(
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(8.dp)
.then(scrollModifier)
) {
content()
}
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(scrollState), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState)
}
}
}