From 15fae29e5b9f8a32a513e5a7c026001f844b3236 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:16:22 +0400 Subject: [PATCH] android, desktop: offer to create 1-time link on address views (#5253) --- .../common/views/newchat/NewChatSheet.kt | 2 +- .../common/views/newchat/NewChatView.kt | 2 +- .../usersettings/UserAddressLearnMore.kt | 62 ++++++- .../views/usersettings/UserAddressView.kt | 153 +++++++++++++----- .../commonMain/resources/MR/base/strings.xml | 14 ++ 5 files changed, 187 insertions(+), 46 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 02996381f8..72118224e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -174,7 +174,7 @@ private fun ModalData.NewChatSheetLayout( val actionButtonsOriginal = listOf( Triple( painterResource(MR.images.ic_add_link), - stringResource(MR.strings.add_contact_tab), + stringResource(MR.strings.create_1_time_link), addContact, ), Triple( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 61403e07a4..e08d46d880 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -98,7 +98,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC val tabTitles = NewChatOption.values().map { when(it) { NewChatOption.INVITE -> - stringResource(MR.strings.add_contact_tab) + stringResource(MR.strings.one_time_link_short) NewChatOption.CONNECT -> stringResource(MR.strings.connect_via_link) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index efc161ac65..ea0cd4fe28 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -3,29 +3,57 @@ package chat.simplex.common.views.usersettings import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.ReadableText -import chat.simplex.common.views.onboarding.ReadableTextWithLink +import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.onboarding.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource @Composable fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false) - ReadableText(MR.strings.you_can_share_your_address) + AppBarTitle(stringResource(MR.strings.address_or_1_time_link), withPadding = false) + + Row { + Icon(painterResource(MR.images.ic_mail), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.share_address_publicly, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.share_simplex_address_on_social_media) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) - ReadableText(MR.strings.you_can_accept_or_reject_connection) - ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address") + + Row(Modifier.padding(top = DEFAULT_PADDING_HALF)) { + Icon(painterResource(MR.images.ic_add_link), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.share_1_time_link_with_a_friend, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.one_time_link_can_be_used_with_one_contact_only) + ReadableText(MR.strings.you_can_set_connection_name_to_remember) + + if (!showCreateAddressButton) { + Row(Modifier.padding(top = DEFAULT_PADDING_HALF)) { + Icon(painterResource(MR.images.ic_shield), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.connection_security, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.simplex_address_and_1_time_links_are_safe_to_share) + ReadableText(MR.strings.to_protect_against_your_link_replaced_compare_codes) + ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses") + } if (showCreateAddressButton) { Spacer(Modifier.weight(1f)) @@ -33,7 +61,7 @@ fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { Button( onClick = { ModalManager.start.showModalCloseable { close -> - UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = { ModalManager.start.closeModals() }) } }, shape = CircleShape, @@ -42,6 +70,24 @@ fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { ) { Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium) } + + val closeAll = { ModalManager.start.closeModals() } + TextButton( + onClick = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + Modifier.padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 2).clip(CircleShape) + ) { + Text( + stringResource(MR.strings.create_1_time_link), + Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index aa9ba70b02..836faee49b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -31,7 +31,6 @@ import chat.simplex.res.MR @Composable fun UserAddressView( chatModel: ChatModel, - viaCreateLinkView: Boolean = false, shareViaProfile: Boolean = false, autoCreateAddress: Boolean = false, close: () -> Unit @@ -39,7 +38,6 @@ fun UserAddressView( // TODO close when remote host changes val shareViaProfile = remember { mutableStateOf(shareViaProfile) } var progressIndicator by remember { mutableStateOf(false) } - val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) } val user = remember { chatModel.currentUser } KeyChangeEffect(user.value?.remoteHostId, user.value?.userId) { close() @@ -82,7 +80,7 @@ fun UserAddressView( } LaunchedEffect(autoCreateAddress) { - if (autoCreateAddress) { + if (chatModel.userAddress.value == null && autoCreateAddress) { createAddress() } } @@ -94,7 +92,6 @@ fun UserAddressView( user = user.value, userAddress = userAddress.value, shareViaProfile, - onCloseHandler, createAddress = { createAddress() }, learnMore = { ModalManager.start.showModal { @@ -105,7 +102,7 @@ fun UserAddressView( sendEmail = { userAddress -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact)) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connReqContact)) ) }, setProfileAddress = ::setProfileAddress, @@ -141,12 +138,8 @@ fun UserAddressView( ) } - if (viaCreateLinkView) { + ModalView(close = close) { showLayout() - } else { - ModalView(close = { onCloseHandler.value(close) }) { - showLayout() - } } if (progressIndicator) { @@ -173,7 +166,6 @@ private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState, - onCloseHandler: MutableState<(close: () -> Unit) -> Unit>, createAddress: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, @@ -190,45 +182,41 @@ private fun UserAddressLayout( verticalArrangement = Arrangement.SpaceEvenly ) { if (userAddress == null) { - SectionView { + SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { CreateAddressButton(createAddress) - SectionTextFooter(stringResource(MR.strings.create_address_and_let_people_connect)) } + + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { LearnMoreButton(learnMore) } - LaunchedEffect(Unit) { - onCloseHandler.value = { close -> close() } - } } else { - val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } - val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } - SectionView(stringResource(MR.strings.address_section_title).uppercase()) { + SectionView(stringResource(MR.strings.for_social_media).uppercase()) { SimpleXLinkQRCode(userAddress.connReqContact) ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } - ShareViaEmailButton { sendEmail(userAddress) } - ShareWithContactsButton(shareViaProfile, setProfileAddress) - AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } - LearnMoreButton(learnMore) + // ShareViaEmailButton { sendEmail(userAddress) } + AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) } - if (autoAcceptState.value.enable) { - SectionDividerSpaced() - AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + LearnMoreButton(learnMore) } SectionDividerSpaced(maxBottomPadding = false) - SectionView { DeleteAddressButton(deleteAddress) SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) } - LaunchedEffect(Unit) { - onCloseHandler.value = { close -> - if (autoAcceptState.value == autoAcceptStateSaved.value) close() - else showUnsavedChangesAlert({ saveAas(autoAcceptState.value, autoAcceptStateSaved); close() }, close) - } - } } } SectionBottomSpacer() @@ -246,11 +234,27 @@ private fun CreateAddressButton(onClick: () -> Unit) { ) } +@Composable +private fun CreateOneTimeLinkButton() { + val closeAll = { ModalManager.start.closeModals() } + SettingsActionItem( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.create_1_time_link), + click = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun LearnMoreButton(onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_info), - stringResource(MR.strings.learn_more_about_address), + stringResource(MR.strings.simplex_address_or_1_time_link), onClick, ) } @@ -266,6 +270,85 @@ fun ShareViaEmailButton(onClick: () -> Unit) { ) } +@Composable +private fun AddressSettingsButton( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState) -> Unit, +) { + SettingsActionItem( + painterResource(MR.images.ic_settings), + stringResource(MR.strings.address_settings), + click = { + ModalManager.start.showCustomModal { close -> + UserAddressSettings(user, userAddress, shareViaProfile, setProfileAddress, saveAas, close = close) + } + } + ) +} + +@Composable +private fun ModalData.UserAddressSettings( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState) -> Unit, + close: () -> Unit +) { + val autoAcceptState = remember { stateGetOrPut("autoAcceptState") { (AutoAcceptState(userAddress)) } } + val autoAcceptStateSaved = remember { stateGetOrPut("autoAcceptStateSaved") { (autoAcceptState.value) } } + + fun onClose(close: () -> Unit): Boolean = if (autoAcceptState.value == autoAcceptStateSaved.value) { + chatModel.centerPanelBackgroundClickHandler = null + close() + false + } else { + showUnsavedChangesAlert( + save = { + saveAas(autoAcceptState.value, autoAcceptStateSaved) + chatModel.centerPanelBackgroundClickHandler = null + close() + }, + revert = { + chatModel.centerPanelBackgroundClickHandler = null + close() + } + ) + true + } + + LaunchedEffect(Unit) { + // Enables unsaved changes alert on this view. + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) + } + } + + ModalView(close = { onClose(close) }) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.address_settings), hostDevice(user?.remoteHostId)) + Column( + Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + SectionView { + ShareWithContactsButton(shareViaProfile, setProfileAddress) + AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } + } + + if (autoAcceptState.value.enable) { + SectionDividerSpaced() + AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + } + } + } + } +} + @Composable fun ShareWithContactsButton(shareViaProfile: MutableState, setProfileAddress: (Boolean) -> Unit) { PreferenceToggleWithIcon( @@ -441,7 +524,6 @@ fun PreviewUserAddressLayoutNoAddress() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } @@ -475,7 +557,6 @@ fun PreviewUserAddressLayoutAddressCreated() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } 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 8b379d8212..5f095fbf7c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -675,10 +675,19 @@ If you can\'t meet in person, show QR code in a video call, or share the link. + Share address publicly + Share SimpleX address on social media. You can share your address as a link or QR code - anybody can connect to you. You won\'t lose your contacts if you later delete your address. + Share 1-time link with a friend + with one contact only - share in person or via any messenger.]]> + You can set connection name, to remember who the link was shared with. + Connection security + SimpleX address and 1-time links are safe to share via any messenger. + To protect against your link being replaced, you can compare contact security codes. When people request to connect, you can accept or reject it. User Guide.]]> + Address or 1-time link? Connect via link @@ -917,6 +926,11 @@ Invite friends Let\'s talk in SimpleX Chat Hi!\nConnect to me via SimpleX Chat: %s + For social media + Or to share privately + SimpleX address or 1-time link? + Create 1-time link + Address settings Continue