From 07191bfb611f0c72a9aa8f727504d8306f01da1e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 22 Sep 2022 23:27:46 +0300 Subject: [PATCH] android: UX for making connections (#1098) * android: UX for making connections * Tabs background color was specified * Different icon and description * update texts, fix layout on ios small screens Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 1 - .../java/chat/simplex/app/model/SimpleXAPI.kt | 3 + .../app/views/newchat/AddContactView.kt | 61 +++++----- .../app/views/newchat/ConnectViaLinkView.kt | 73 +++++++++++ .../app/views/newchat/CreateLinkView.kt | 89 ++++++++++++++ .../simplex/app/views/newchat/NewChatSheet.kt | 45 ++----- .../app/views/newchat/PasteToConnect.kt | 92 +++++++------- .../app/views/newchat/ScanToConnectView.kt | 68 +++++------ .../app/views/usersettings/SettingsView.kt | 4 +- .../app/views/usersettings/UserAddressView.kt | 2 +- .../app/src/main/res/values-ru/strings.xml | 40 +++++-- .../app/src/main/res/values/strings.xml | 35 ++++-- .../Views/NewChat/PasteToConnectView.swift | 113 +++++++++--------- .../Views/NewChat/ScanToConnectView.swift | 56 ++++----- 14 files changed, 428 insertions(+), 254 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index bf3fec663c..385744ff8a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -37,7 +37,6 @@ class ChatModel(val controller: ChatController) { val chatItems = mutableStateListOf() val groupMembers = mutableStateListOf() - var connReqInvitation: String? = null val terminalItems = mutableStateListOf() val userAddress = mutableStateOf(null) val userSMPServers = mutableStateOf<(List)?>(null) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 18657e539c..dd4aacf5b2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -25,6 +25,7 @@ import chat.simplex.app.R import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.newchat.ConnectViaLinkTab import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.usersettings.NotificationPreviewMode import chat.simplex.app.views.usersettings.NotificationsMode @@ -106,6 +107,7 @@ class AppPreferences(val context: Context) { val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl) val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt) val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) + val connectViaLinkTab = mkStrPreference(SHARED_PREFS_CONNECT_VIA_LINK_TAB, ConnectViaLinkTab.SCAN.name) val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true) val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false) @@ -194,6 +196,7 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl" private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" private const val SHARED_PREFS_INCOGNITO = "Incognito" + private const val SHARED_PREFS_CONNECT_VIA_LINK_TAB = "ConnectViaLinkTab" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase" diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt index b2a5bb8bbd..4acb441a79 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -2,6 +2,8 @@ package chat.simplex.app.views.newchat import android.content.res.Configuration import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.TheaterComedy @@ -12,7 +14,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -20,20 +21,16 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.generalGetString -import chat.simplex.app.views.helpers.shareText +import chat.simplex.app.views.helpers.* @Composable -fun AddContactView(chatModel: ChatModel) { - val connReq = chatModel.connReqInvitation - if (connReq != null) { - val cxt = LocalContext.current - AddContactLayout( - chatModelIncognito = chatModel.incognito.value, - connReq = connReq, - share = { shareText(cxt, connReq) } - ) - } +fun AddContactView(chatModel: ChatModel, connReqInvitation: String) { + val cxt = LocalContext.current + AddContactLayout( + chatModelIncognito = chatModel.incognito.value, + connReq = connReqInvitation, + share = { shareText(cxt, connReqInvitation) } + ) } @Composable @@ -41,21 +38,18 @@ fun AddContactLayout(chatModelIncognito: Boolean, connReq: String, share: () -> BoxWithConstraints { val screenHeight = maxHeight Column( - Modifier.padding(bottom = 16.dp), + Modifier.verticalScroll(rememberScrollState()).padding(bottom = 16.dp), verticalArrangement = Arrangement.SpaceBetween, ) { Text( stringResource(R.string.add_contact), - style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), - modifier = Modifier - .padding(vertical = 5.dp) - .padding(horizontal = 8.dp) + Modifier.padding(bottom = 16.dp), + style = MaterialTheme.typography.h1, ) Text( stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline), - modifier = Modifier.padding(horizontal = 8.dp) ) - Row(Modifier.padding(horizontal = 8.dp)) { + Row { InfoAboutIncognito( chatModelIncognito, true, @@ -63,18 +57,27 @@ fun AddContactLayout(chatModelIncognito: Boolean, connReq: String, share: () -> generalGetString(R.string.your_profile_will_be_sent) ) } - QRCode( - connReq, Modifier - .weight(1f, fill = false) - .aspectRatio(1f) - .padding(vertical = 3.dp) - ) + if (connReq.isNotEmpty()) { + QRCode( + connReq, Modifier + .aspectRatio(1f) + .padding(vertical = 3.dp) + ) + } else { + CircularProgressIndicator( + Modifier + .size(36.dp) + .padding(4.dp) + .align(Alignment.CenterHorizontally), + color = HighOrLowlight, + strokeWidth = 3.dp + ) + } Text( - stringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel), + annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel), lineHeight = 22.sp, modifier = Modifier - .padding(horizontal = 8.dp) - .padding(bottom = if (screenHeight > 600.dp) 16.dp else 8.dp) + .padding(bottom = if (screenHeight > 600.dp) 8.dp else 0.dp) ) Row( Modifier.fillMaxWidth(), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt new file mode 100644 index 0000000000..892ff5672f --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt @@ -0,0 +1,73 @@ +package chat.simplex.app.views.newchat + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.views.helpers.withApi +import chat.simplex.app.views.usersettings.UserAddressView + +enum class ConnectViaLinkTab { + SCAN, PASTE +} + +@Composable +fun ConnectViaLinkView(m: ChatModel) { + val selection = remember { + mutableStateOf( + runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN) + ) + } + val tabTitles = ConnectViaLinkTab.values().map { + when (it) { + ConnectViaLinkTab.SCAN -> stringResource(R.string.scan_QR_code) + ConnectViaLinkTab.PASTE -> stringResource(R.string.paste_the_link_you_received) + } + } + Column( + Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column(Modifier.weight(1f)) { + when (selection.value) { + ConnectViaLinkTab.SCAN -> { + ScanToConnectView(m) + } + ConnectViaLinkTab.PASTE -> { + PasteToConnectView(m) + } + } + } + TabRow( + selectedTabIndex = selection.value.ordinal, + backgroundColor = MaterialTheme.colors.background, + contentColor = MaterialTheme.colors.primary, + ) { + tabTitles.forEachIndexed { index, it -> + Tab( + selected = selection.value.ordinal == index, + onClick = { + selection.value = ConnectViaLinkTab.values()[index] + m.controller.appPrefs.connectViaLinkTab.set(selection.value .name) + }, + text = { Text(it, fontSize = 13.sp) }, + icon = { + Icon( + if (ConnectViaLinkTab.SCAN.ordinal == index) Icons.Outlined.QrCode else Icons.Outlined.Article, + it + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = HighOrLowlight, + ) + } + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt new file mode 100644 index 0000000000..0908aca35b --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt @@ -0,0 +1,89 @@ +package chat.simplex.app.views.newchat + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.views.helpers.withApi +import chat.simplex.app.views.usersettings.UserAddressView + +enum class CreateLinkTab { + ONE_TIME, LONG_TERM +} + +@Composable +fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { + val selection = remember { mutableStateOf(initialSelection) } + val connReqInvitation = remember { mutableStateOf("") } + val creatingConnReq = remember { mutableStateOf(false) } + LaunchedEffect(selection.value) { + if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() && !creatingConnReq.value) { + createInvitation(m, creatingConnReq, connReqInvitation) + } + } + val tabTitles = CreateLinkTab.values().map { + when { + it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link) + it == CreateLinkTab.ONE_TIME -> stringResource(R.string.one_time_link) + it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_contact_address) + else -> "" + } + } + Column( + Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column(Modifier.weight(1f)) { + when (selection.value) { + CreateLinkTab.ONE_TIME -> { + AddContactView(m, connReqInvitation.value) + } + CreateLinkTab.LONG_TERM -> { + UserAddressView(m) + } + } + } + TabRow( + selectedTabIndex = selection.value.ordinal, + backgroundColor = MaterialTheme.colors.background, + contentColor = MaterialTheme.colors.primary, + ) { + tabTitles.forEachIndexed { index, it -> + Tab( + selected = selection.value.ordinal == index, + onClick = { + selection.value = CreateLinkTab.values()[index] + }, + text = { Text(it, fontSize = 13.sp) }, + icon = { + Icon( + if (CreateLinkTab.ONE_TIME.ordinal == index) Icons.Outlined.RepeatOne else Icons.Outlined.AllInclusive, + it + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = HighOrLowlight, + ) + } + } + } +} + +private fun createInvitation(m: ChatModel, creatingConnReq: MutableState, connReqInvitation: MutableState) { + creatingConnReq.value = true + withApi { + val connReq = m.controller.apiAddContact() + if (connReq != null) { + connReqInvitation.value = connReq + } else { + creatingConnReq.value = false + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt index 8186fb25e0..81c19a57f8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -1,6 +1,5 @@ package chat.simplex.app.views.newchat -import android.Manifest import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -22,32 +21,17 @@ import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chatlist.ScaffoldController import chat.simplex.app.views.helpers.ModalManager -import chat.simplex.app.views.helpers.withApi -import com.google.accompanist.permissions.rememberPermissionState @Composable fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController) { - val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) NewChatSheetLayout( addContact = { - withApi { - // show spinner - chatModel.connReqInvitation = chatModel.controller.apiAddContact() - // hide spinner - if (chatModel.connReqInvitation != null) { - newChatCtrl.collapse() - ModalManager.shared.showModal { AddContactView(chatModel) } - } - } - }, - scanCode = { newChatCtrl.collapse() - ModalManager.shared.showCustomModal { close -> ScanToConnectView(chatModel, close) } - cameraPermissionState.launchPermissionRequest() + ModalManager.shared.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) } }, - pasteLink = { + connectViaLink = { newChatCtrl.collapse() - ModalManager.shared.showCustomModal { close -> PasteToConnectView(chatModel, close) } + ModalManager.shared.showModal { ConnectViaLinkView(chatModel) } }, createGroup = { newChatCtrl.collapse() @@ -59,8 +43,7 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController) { @Composable fun NewChatSheetLayout( addContact: () -> Unit, - scanCode: () -> Unit, - pasteLink: () -> Unit, + connectViaLink: () -> Unit, createGroup: () -> Unit ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -73,7 +56,7 @@ fun NewChatSheetLayout( Divider(Modifier.padding(horizontal = 8.dp)) Box(boxModifier) { ActionRowButton( - stringResource(R.string.create_one_time_link), + stringResource(R.string.share_one_time_link), stringResource(R.string.to_share_with_your_contact), Icons.Outlined.AddLink, click = addContact @@ -82,19 +65,10 @@ fun NewChatSheetLayout( Divider(Modifier.padding(horizontal = 8.dp)) Box(boxModifier) { ActionRowButton( - stringResource(R.string.paste_received_link), - stringResource(R.string.paste_received_link_from_clipboard), - Icons.Outlined.Article, - click = pasteLink - ) - } - Divider(Modifier.padding(horizontal = 8.dp)) - Box(boxModifier) { - ActionRowButton( - stringResource(R.string.scan_QR_code), - stringResource(R.string.in_person_or_in_video_call__bracketed), + stringResource(R.string.connect_via_link_or_qr), + stringResource(R.string.connect_via_link_or_qr_from_clipboard_or_in_person), Icons.Outlined.QrCode, - click = scanCode + click = connectViaLink ) } Divider(Modifier.padding(horizontal = 8.dp)) @@ -189,8 +163,7 @@ fun PreviewNewChatSheet() { SimpleXTheme { NewChatSheetLayout( addContact = {}, - scanCode = {}, - pasteLink = {}, + connectViaLink = {}, createGroup = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt index bfb62fd89b..ccf32033c6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt @@ -3,18 +3,17 @@ package chat.simplex.app.views.newchat import android.content.ClipboardManager import android.content.res.Configuration import android.net.Uri -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.getSystemService @@ -25,11 +24,10 @@ import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.* @Composable -fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { - val connectionLink = remember { mutableStateOf("")} +fun PasteToConnectView(chatModel: ChatModel) { + val connectionLink = remember { mutableStateOf("") } val context = LocalContext.current val clipboard = getSystemService(context, ClipboardManager::class.java) - BackHandler(onBack = close) PasteToConnectLayout( chatModel.incognito.value, connectionLink = connectionLink, @@ -48,9 +46,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { text = generalGetString(R.string.this_string_is_not_a_connection_link) ) } - close() }, - close = close ) } @@ -60,52 +56,49 @@ fun PasteToConnectLayout( connectionLink: MutableState, pasteFromClipboard: () -> Unit, connectViaLink: (String) -> Unit, - close: () -> Unit ) { - ModalView(close) { - Column( + Column( + Modifier.verticalScroll(rememberScrollState()).padding(bottom = 16.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Text( + stringResource(R.string.connect_via_link), Modifier.padding(bottom = 16.dp), - verticalArrangement = Arrangement.SpaceBetween, - ) { - Text( - stringResource(R.string.connect_via_link), - style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), - modifier = Modifier.padding(vertical = 5.dp) - ) - Text(stringResource(R.string.paste_connection_link_below_to_connect)) + style = MaterialTheme.typography.h1, + ) + Text(stringResource(R.string.paste_connection_link_below_to_connect)) - InfoAboutIncognito( - chatModelIncognito, - true, - generalGetString(R.string.incognito_random_profile_from_contact_description), - generalGetString(R.string.profile_will_be_sent_to_contact_sending_link) - ) + InfoAboutIncognito( + chatModelIncognito, + true, + generalGetString(R.string.incognito_random_profile_from_contact_description), + generalGetString(R.string.profile_will_be_sent_to_contact_sending_link) + ) - Box(Modifier.padding(top = 16.dp, bottom = 6.dp)) { - TextEditor(Modifier.height(180.dp), text = connectionLink) - } - - Row( - Modifier.fillMaxWidth().padding(bottom = 6.dp), - horizontalArrangement = Arrangement.Start, - ) { - if (connectionLink.value == "") { - SimpleButton(text = "Paste", icon = Icons.Outlined.ContentPaste) { - pasteFromClipboard() - } - } else { - SimpleButton(text = "Clear", icon = Icons.Outlined.Clear) { - connectionLink.value = "" - } - } - Spacer(Modifier.weight(1f).fillMaxWidth()) - SimpleButton(text = "Connect", icon = Icons.Outlined.Link) { - connectViaLink(connectionLink.value) - } - } - - Text(annotatedStringResource(R.string.you_can_also_connect_by_clicking_the_link)) + Box(Modifier.padding(top = 16.dp, bottom = 6.dp)) { + TextEditor(Modifier.height(180.dp), text = connectionLink) } + + Row( + Modifier.fillMaxWidth().padding(bottom = 6.dp), + horizontalArrangement = Arrangement.Start, + ) { + if (connectionLink.value == "") { + SimpleButton(text = stringResource(R.string.paste_button), icon = Icons.Outlined.ContentPaste) { + pasteFromClipboard() + } + } else { + SimpleButton(text = stringResource(R.string.clear_verb), icon = Icons.Outlined.Clear) { + connectionLink.value = "" + } + } + Spacer(Modifier.weight(1f).fillMaxWidth()) + SimpleButton(text = stringResource(R.string.connect_button), icon = Icons.Outlined.Link) { + connectViaLink(connectionLink.value) + } + } + + Text(annotatedStringResource(R.string.you_can_also_connect_by_clicking_the_link)) } } @@ -130,7 +123,6 @@ fun PreviewPasteToConnectTextbox() { e.printStackTrace() } }, - close = {} ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt index 465591c544..98b487dc81 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt @@ -1,14 +1,15 @@ package chat.simplex.app.views.newchat +import android.Manifest import android.content.res.Configuration import android.net.Uri -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -16,10 +17,14 @@ import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.* +import com.google.accompanist.permissions.rememberPermissionState @Composable -fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { - BackHandler(onBack = close) +fun ScanToConnectView(chatModel: ChatModel) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) + LaunchedEffect(Unit) { + cameraPermissionState.launchPermissionRequest() + } ConnectContactLayout( chatModelIncognito = chatModel.incognito.value, qrCodeScanner = { @@ -35,10 +40,8 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { text = generalGetString(R.string.this_QR_code_is_not_a_link) ) } - close() } }, - close = close ) } @@ -67,33 +70,31 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) { } @Composable -fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { - ModalView(close) { - Column( +fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit) { + Column( + Modifier.verticalScroll(rememberScrollState()).padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + generalGetString(R.string.scan_QR_code), Modifier.padding(bottom = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - generalGetString(R.string.scan_QR_code), - style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), - modifier = Modifier.padding(vertical = 5.dp) - ) - InfoAboutIncognito( - chatModelIncognito, - true, - generalGetString(R.string.incognito_random_profile_description), - generalGetString(R.string.your_profile_will_be_sent) - ) - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - ) { qrCodeScanner() } - Text( - annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link), - lineHeight = 22.sp - ) - } + style = MaterialTheme.typography.h1, + ) + InfoAboutIncognito( + chatModelIncognito, + true, + generalGetString(R.string.incognito_random_profile_description), + generalGetString(R.string.your_profile_will_be_sent) + ) + Box( + Modifier + .fillMaxWidth() + .aspectRatio(ratio = 1F) + ) { qrCodeScanner() } + Text( + annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link), + lineHeight = 22.sp + ) } } @@ -109,7 +110,6 @@ fun PreviewConnectContactLayout() { ConnectContactLayout( chatModelIncognito = false, qrCodeScanner = { Surface {} }, - close = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index fb6bcb49ed..e560d42d77 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -31,6 +31,8 @@ import chat.simplex.app.ui.theme.* import chat.simplex.app.views.TerminalView import chat.simplex.app.views.database.DatabaseView import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.newchat.CreateLinkTab +import chat.simplex.app.views.newchat.CreateLinkView import chat.simplex.app.views.onboarding.SimpleXInfo @Composable @@ -113,7 +115,7 @@ fun SettingsLayout( SectionDivider() SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { onClickIncognitoInfo(showModal) } SectionDivider() - SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped) + SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped) SectionDivider() DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt index 71e5b37033..06b6b5e5bb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt @@ -61,7 +61,7 @@ fun UserAddressLayout( verticalArrangement = Arrangement.Top ) { Text( - stringResource(R.string.your_chat_address), + stringResource(R.string.your_contact_address), Modifier.padding(bottom = 16.dp), style = MaterialTheme.typography.h1, ) diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index e70f9e3977..7d62acb85a 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -216,18 +216,17 @@ Подтвердить OK нет описания - Добавить контакт + Одноразовая ссылка Скопировано в буфер обмена Начать новый разговор - Создать одноразовую ссылку - Вставить полученную ссылку - Сканировать QR код + Создать ссылку-приглашение + Соединиться через ссылку / QR код + Сканировать\nQR код Создать секретную группу (чтобы отправить вашему контакту) - (при встрече или через видеозвонок) - (вставить ссылку из буфера обмена) + (сканировать или вставить из буфера) (хранится только у членов группы) @@ -306,14 +305,22 @@ Запрос на соединение послан! Соединение будет установлено когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! Соединение будет установлено когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже! - Покажите QR код вашему контакту, чтобы сосканировать его из приложения. - Если вы не можете встретиться лично, вы можете показать QR код во время видеозвонка или отправить ссылку через любой другой канал связи. + Ваш контакт может сосканировать QR в приложении. + Если вы не можете встретиться лично, вы можете показать QR код во время видеозвонка или поделиться ссылкой. Ваш профиль будет отправлен\nвашему контакту Если вы не можете встретиться лично, вы можете сосканировать QR код во время видеозвонка, или ваш контакт может отправить вам ссылку. Поделиться ссылкой Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта. Ваш профиль будет отправлен вашему контакту + + Соединиться + Вставить + + + Создать одноразовую ссылку + Одноразовая ссылка + Ваш SimpleX адрес Настройки @@ -368,8 +375,7 @@ Создать адрес Удалить адрес? Все контакты, которые соединились через этот адрес, сохранятся. - Ваш SimpleX адрес - Вы можете использовать адрес как ссылку или как QR код - через него можно с вами соединиться. + Вы можете использовать адрес как ссылку или как QR код - через него можно с вами соединиться. Вы сможете удалить адрес, сохранив контакты, которые через него соединились. Вы сможете удалить адрес, сохранив контакты, которые через него соединились. Поделиться\nссылкой Удалить\nадрес @@ -407,7 +413,7 @@ секрет Соединиться через ссылку Эта строка не является ссылкой-приглашением! - Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку Open in mobile app. + Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку Открыть в приложении. входящий звонок… @@ -450,6 +456,18 @@ Узнайте больше из нашего GitHub репозитория. Узнайте больше из нашего GitHub репозитория. + + Чтобы добавить ваш первый контакт, выберите одно из: + Создать ссылку / QR код + Ей безопасно поделиться - только один контакт может использовать её. + Вставить полученную ссылку + Или откройте ссылку в браузере и нажмите Open in mobile. + Сосканировать QR код контакта + При встрече или в видеозвонке – самый безопасный способ установить соединение + или + Соединиться с разработчиками + Чтобы задать вопросы и получать уведомления о SimpleX Chat. + Входящий видеозвонок Входящий аудиозвонок diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 994339cc4d..02663d77bc 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -216,18 +216,17 @@ Confirm OK no details - Add contact + One-time invitation link Copied to clipboard Start new chat - Create link / QR code - Connect via received link + Create one-time invitation link + Connect via link / QR code Scan QR code Create secret group (to share with your contact) - (in person or in video call) - (paste link from clipboard) + (scan or paste from clipboard) (only stored by group members) @@ -306,8 +305,8 @@ Connection request sent! You will be connected when your connection request is accepted, please wait or check later! You will be connected when your contact\'s device is online, please wait or check later! - Show QR code for your contact to scan from the app. - If you can\'t meet in person, you can show QR code in the video call, or you can share the invitation link via any other channel. + Your contact can scan it from the app. + If you can\'t meet in person, show QR code in the video call, or share the link. Your chat profile will be sent\nto your contact If you cannot meet in person, you can scan QR code in the video call, or your contact can share an invitation link. Share invitation link @@ -316,9 +315,16 @@ Connect via link + Connect + Paste This string is not a connection link! You can also connect by clicking the link. If it opens in the browser, click Open in mobile app button. + + Create one-time invitation link + One-time invitation link + Your contact address + Your settings Your SimpleX contact address @@ -372,8 +378,7 @@ Create address Delete address? All your contacts will remain connected. - Your chat address - You can share your address as a link or as a QR code - anybody will be able to connect to you. + You can share your address as a link or as a QR code - anybody will be able to connect to you. You won\'t lose your contacts if you later delete it. If you later delete it - you won\'t lose your contacts. Share link Delete address @@ -451,6 +456,18 @@ Read more in our GitHub repository. Read more in our GitHub repository. + + To make your first private connection, choose one of the following: + Create 1-time link / QR code + It\'s secure to share - only one contact can use it. + Paste received link + Or open the link in the browser and tap Open in mobile. + Scan contact\'s QR code + In person or via a video call – the most secure way to connect. + or + Connect with the developers + To ask any questions and to receive SimpleX Chat updates. + Incoming video call Incoming audio call diff --git a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift index 3b45a4eb89..4be3f91ce0 100644 --- a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift @@ -14,67 +14,70 @@ struct PasteToConnectView: View { @State private var connectionLink: String = "" var body: some View { - VStack(alignment: .leading) { - Text("Connect via link") - .font(.largeTitle) - .bold() - .padding(.vertical) - Text("Paste the link you received into the box below to connect with your contact.") - .padding(.bottom, 4) - if (chatModel.incognito) { - HStack { - Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) - Spacer().frame(width: 8) - Text("A random profile will be sent to the contact that you received this link from").font(.footnote) - } - .padding(.bottom) - } else { - HStack { - Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) - Spacer().frame(width: 8) - Text("Your profile will be sent to the contact that you received this link from").font(.footnote) - } - .padding(.bottom) - } - TextEditor(text: $connectionLink) - .onSubmit(connect) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .allowsTightening(false) - .frame(height: 180) - .overlay( - RoundedRectangle(cornerRadius: 10) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) - ) - - HStack(spacing: 20) { - if connectionLink == "" { - Button { - connectionLink = UIPasteboard.general.string ?? "" - } label: { - Label("Paste", systemImage: "doc.plaintext") + ScrollView { + VStack(alignment: .leading) { + Text("Connect via link") + .font(.largeTitle) + .bold() + .fixedSize(horizontal: false, vertical: true) + .padding(.vertical) + Text("Paste the link you received into the box below to connect with your contact.") + .padding(.bottom, 4) + if (chatModel.incognito) { + HStack { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("A random profile will be sent to the contact that you received this link from").font(.footnote) } + .padding(.bottom) } else { - Button { - connectionLink = "" - } label: { - Label("Clear", systemImage: "multiply") + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your profile will be sent to the contact that you received this link from").font(.footnote) } - + .padding(.bottom) } - Spacer() - Button(action: connect, label: { - Label("Connect", systemImage: "link") - }) - .disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil) - } - .frame(height: 48) - .padding(.bottom) + TextEditor(text: $connectionLink) + .onSubmit(connect) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .allowsTightening(false) + .frame(height: 180) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) + ) - Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") + HStack(spacing: 20) { + if connectionLink == "" { + Button { + connectionLink = UIPasteboard.general.string ?? "" + } label: { + Label("Paste", systemImage: "doc.plaintext") + } + } else { + Button { + connectionLink = "" + } label: { + Label("Clear", systemImage: "multiply") + } + + } + Spacer() + Button(action: connect, label: { + Label("Connect", systemImage: "link") + }) + .disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil) + } + .frame(height: 48) + .padding(.bottom) + + Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } private func connect() { diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift index e9836e6eb8..2725faed07 100644 --- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift @@ -14,37 +14,39 @@ struct ScanToConnectView: View { @Environment(\.dismiss) var dismiss: DismissAction var body: some View { - VStack(alignment: .leading) { - Text("Scan QR code") - .font(.largeTitle) - .bold() - .padding(.vertical) - if (chatModel.incognito) { - HStack { - Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) - Spacer().frame(width: 8) - Text("A random profile will be sent to your contact").font(.footnote) - } - .padding(.bottom) - } else { - HStack { - Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) - Spacer().frame(width: 8) - Text("Your chat profile will be sent to your contact").font(.footnote) + ScrollView { + VStack(alignment: .leading) { + Text("Scan QR code") + .font(.largeTitle) + .bold() + .padding(.vertical) + if (chatModel.incognito) { + HStack { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("A random profile will be sent to your contact").font(.footnote) + } + .padding(.bottom) + } else { + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your chat profile will be sent to your contact").font(.footnote) + } + .padding(.bottom) + } + ZStack { + CodeScannerView(codeTypes: [.qr], completion: processQRCode) + .aspectRatio(1, contentMode: .fit) + .border(.gray) } .padding(.bottom) + Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.") + .padding(.bottom) } - ZStack { - CodeScannerView(codeTypes: [.qr], completion: processQRCode) - .aspectRatio(1, contentMode: .fit) - .border(.gray) - } - .padding(.bottom) - Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.") - .padding(.bottom) + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } func processQRCode(_ resp: Result) {