From c04e952620ac3b6a3a01724d88607485837121d3 Mon Sep 17 00:00:00 2001 From: Diogo Date: Mon, 2 Dec 2024 21:00:55 +0000 Subject: [PATCH] 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 Co-authored-by: Avently <7953703+avently@users.noreply.github.com> --- .../networkAndServers/OperatorView.android.kt | 19 ++++ .../common/views/database/DatabaseView.kt | 1 + .../views/onboarding/ChooseServerOperators.kt | 8 +- .../common/views/onboarding/HowItWorks.kt | 8 +- .../onboarding/SetupDatabasePassphrase.kt | 94 +++++++++---------- .../networkAndServers/NetworkAndServers.kt | 2 +- .../networkAndServers/OperatorView.kt | 27 +++--- .../networkAndServers/OperatorView.desktop.kt | 51 ++++++++++ 8 files changed, 141 insertions(+), 69 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt new file mode 100644 index 0000000000..e52515b345 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt @@ -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() + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index af40aa2e70..e1f53760e5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 84ed7d1651..782f51c205 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -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>, selectedOperatorIds: State>, 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_ }) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index f7e7f456bb..aff02e90f5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -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? = 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? 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)) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index f20cb38dad..e7db51c768 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -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, + ) +} 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 6b1dcec5d8..835e01ec27 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 @@ -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()) { 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 4e0fc6ec79..3836d5b6df 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 @@ -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, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt new file mode 100644 index 0000000000..c9f62a5b79 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt @@ -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 = 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) + } + } +}