diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/ConnectBannerCard.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/ConnectBannerCard.kt new file mode 100644 index 0000000000..4db39293e3 --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/ConnectBannerCard.kt @@ -0,0 +1,136 @@ +package chat.simplex.app.views.invitation_redesign + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* +import chat.simplex.res.MR + +@Composable +fun ConnectBannerCard() { + val closeAll = { ModalManager.start.closeModals() } + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier.fillMaxWidth() + ) { + Box { + Column { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Image( + painterResource(MR.images.ic_invitation_card_invite_someone), + contentDescription = stringResource(MR.strings.create_link_or_qr), + modifier = Modifier + .weight(1f) + .clickable { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + contentScale = ContentScale.FillWidth + ) + Image( + painterResource(MR.images.ic_invitation_card_one_time_link), + contentDescription = stringResource(MR.strings.paste_link_scan), + modifier = Modifier + .weight(1f) + .clickable { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) + } + }, + contentScale = ContentScale.FillWidth + ) + } + Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.06f)) + Row( + Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING_HALF), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Row( + Modifier + .weight(1f) + .clickable { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_repeat_one), + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + stringResource(MR.strings.create_link_or_qr), + style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Medium), + ) + } + Row( + Modifier + .weight(1f) + .clickable { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) + } + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_qr_code), + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + stringResource(MR.strings.paste_link_scan), + style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Medium), + ) + } + } + } + IconButton( + onClick = { appPrefs.connectBannerCardShown.set(true) }, + modifier = Modifier.align(Alignment.TopEnd).padding(4.dp) + ) { + Icon( + painterResource(MR.images.ic_close), + stringResource(MR.strings.back), + tint = MaterialTheme.colors.secondary + ) + } + } + } +} + +@Preview +@Composable +fun PreviewConnectBannerCard() { + SimpleXTheme { + ConnectBannerCard() + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/ConnectViewLinkOrQrModal.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/ConnectViewLinkOrQrModal.kt new file mode 100644 index 0000000000..8374c7adf1 --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/ConnectViewLinkOrQrModal.kt @@ -0,0 +1,147 @@ +package chat.simplex.app.views.invitation_redesign + +import SectionBottomSpacer +import SectionItemView +import SectionView +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.CIFileViewScope +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* +import chat.simplex.res.MR + +@Composable +fun ModalData.ConnectViewLinkOrQrModal(rhId: Long?, close: () -> Unit) { + val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { true } } + val pastedLink = rememberSaveable { mutableStateOf("") } + + DisposableEffect(Unit) { + onDispose { + connectProgressManager.cancelConnectProgress() + } + } + + ModalView(close) { + ColumnWithScrollBar( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppBarTitle(stringResource(MR.strings.connect_via_link), withPadding = false) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + Icon( + painterResource(MR.images.ic_add_link), + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colors.primary + ) + + Spacer(Modifier.height(DEFAULT_PADDING * 1.5f)) + + SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { + ConnectPasteLinkView(rhId, pastedLink, showQRCodeScanner, close) + } + + if (appPlatform.isAndroid) { + Spacer(Modifier.height(10.dp)) + + SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) { + Box(Modifier.clip(RoundedCornerShape(DEFAULT_PADDING))) { + QRCodeScanner(showQRCodeScanner) { text -> + val linkVerified = strIsSimplexLink(text) + if (!linkVerified) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_qr_code), + text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code) + ) + } + connectFromScanner(rhId, text, close) + } + } + } + } + + SectionBottomSpacer() + } + } +} + +@Composable +private fun ConnectPasteLinkView(rhId: Long?, pastedLink: MutableState, showQRCodeScanner: MutableState, close: () -> Unit) { + if (pastedLink.value.isEmpty()) { + val clipboard = LocalClipboardManager.current + SectionItemView({ + val str = clipboard.getText()?.text ?: return@SectionItemView + val link = strHasSingleSimplexLink(str.trim()) + if (link != null) { + pastedLink.value = link.text + showQRCodeScanner.value = false + withBGApi { + connectFromPaste(rhId, link.text, close) { pastedLink.value = "" } + } + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_contact_link), + text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) + ) + } + }) { + Box(Modifier.weight(1f)) { + Text(stringResource(MR.strings.tap_to_paste_link)) + } + if (connectProgressManager.showConnectProgress != null) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) + } + } + } else { + Row( + Modifier.padding(end = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.weight(1f)) { + LinkTextView(pastedLink.value, false) + } + if (connectProgressManager.showConnectProgress != null) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) + } + } + } +} + +private suspend fun connectFromScanner(rhId: Long?, text: String?, close: () -> Unit): Boolean { + if (text != null && strIsSimplexLink(text)) { + return connectFromPaste(rhId, text, close) + } + return false +} + +private suspend fun connectFromPaste(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null): Boolean = + planAndConnect( + rhId, + link, + close = close, + cleanup = cleanup + ).await() + +@Preview +@Composable +fun PreviewConnectViewLinkOrQrModal() { + SimpleXTheme { + ModalData().ConnectViewLinkOrQrModal(rhId = null, close = {}) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/EmptyChatListView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/EmptyChatListView.kt new file mode 100644 index 0000000000..f5eea4309c --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/EmptyChatListView.kt @@ -0,0 +1,141 @@ +package chat.simplex.app.views.invitation_redesign + +import SectionBottomSpacer +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* +import chat.simplex.res.MR + +@Composable +fun BoxScope.EmptyChatListView() { + var showInviteSomeone by remember { mutableStateOf(false) } + + if (showInviteSomeone) { + BackHandler { showInviteSomeone = false } + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(Modifier.fillMaxWidth()) { + NavigationButtonBack(onButtonClicked = { showInviteSomeone = false }) + } + Text( + stringResource(MR.strings.invite_someone), + style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Bold), + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + InviteSomeoneContent() + SectionBottomSpacer() + } + } else { + val closeAll = { ModalManager.start.closeModals() } + Column( + Modifier + .align(Alignment.Center) + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + stringResource(MR.strings.now_you_can), + style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Bold), + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier.fillMaxWidth().clickable { + showInviteSomeone = true + } + ) { + Column( + Modifier.fillMaxWidth().padding(DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painterResource(MR.images.ic_invitation_card_invite_someone), + contentDescription = null, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(MR.images.ic_repeat_one), + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + stringResource(MR.strings.invite_someone_to_chat), + style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium), + ) + } + } + } + Spacer(Modifier.height(DEFAULT_PADDING)) + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier.fillMaxWidth().clickable { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) + } + } + ) { + Column( + Modifier.fillMaxWidth().padding(DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painterResource(MR.images.ic_invitation_card_one_time_link), + contentDescription = null, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(MR.images.ic_qr_code), + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + stringResource(MR.strings.connect_via_link_or_qr), + style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium), + ) + } + } + } + } + } +} + +@Preview +@Composable +fun PreviewEmptyChatListView() { + SimpleXTheme { + Box(Modifier.fillMaxSize()) { + EmptyChatListView() + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/InviteSomeoneView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/InviteSomeoneView.kt new file mode 100644 index 0000000000..52cd13ab35 --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/InviteSomeoneView.kt @@ -0,0 +1,160 @@ +package chat.simplex.app.views.invitation_redesign + +import SectionBottomSpacer +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.invitation_redesign.OneTimeLinkView +import chat.simplex.common.views.usersettings.UserAddressView +import chat.simplex.res.MR + +@Composable +fun InviteSomeoneView(close: () -> Unit) { + ModalView(close) { + ColumnWithScrollBar( + Modifier.fillMaxSize().background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppBarTitle(stringResource(MR.strings.invite_someone), withPadding = false) + Spacer(Modifier.height(DEFAULT_PADDING)) + InviteSomeoneContent() + SectionBottomSpacer() + } + } +} + +@Composable +fun InviteSomeoneContent() { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier + .fillMaxSize().background(Color.White) + .padding(horizontal = DEFAULT_PADDING) + .clickable { + ModalManager.start.showModalCloseable { close -> + OneTimeLinkView(rhId = chatModel.currentRemoteHost.value?.remoteHostId, close = close) + } + } + ) { + Column( + Modifier.fillMaxWidth().padding(DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painterResource(MR.images.ic_invitation_card_one_time_link), + contentDescription = null, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_repeat_one), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Column { + Text( + stringResource(MR.strings.create_private_1_time_link), + style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold), + ) + Text( + stringResource(MR.strings.contact_can_use_link_or_scan_qr), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary + ) + } + } + + Spacer(Modifier.height(DEFAULT_PADDING * 1.5f)) + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + .clickable { + ModalManager.start.showModalCloseable { closeAddress -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = closeAddress) + } + } + ) { + Column( + Modifier.fillMaxWidth().padding(DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painterResource(MR.images.ic_invitation_card_public_address), + contentDescription = null, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_qr_code), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Column { + Text( + stringResource(MR.strings.create_public_simplex_address), + style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold), + ) + Text( + stringResource(MR.strings.public_link_for_social_media_email_or_website), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary + ) + } + } +} + +@Preview +@Composable +fun PreviewInviteSomeoneView() { + SimpleXTheme { + ColumnWithScrollBar( + Modifier.fillMaxSize().background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppBarTitle(stringResource(MR.strings.invite_someone), withPadding = false) + Spacer(Modifier.height(DEFAULT_PADDING)) + InviteSomeoneContent() + SectionBottomSpacer() + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/OneTimeLinkView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/OneTimeLinkView.kt new file mode 100644 index 0000000000..3c1b07b96c --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/invitation_redesign/OneTimeLinkView.kt @@ -0,0 +1,219 @@ +package chat.simplex.app.views.invitation_redesign + +import SectionBottomSpacer +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +@Composable +fun OneTimeLinkView(rhId: Long?, close: () -> Unit) { + val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } + val connLinkInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connLink ?: CreatedConnLink("", null) } } + val creatingConnReq = rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if ( + connLinkInvitation.connFullLink.isEmpty() + && contactConnection.value == null + && !creatingConnReq.value + ) { + creatingConnReq.value = true + withBGApi { + val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, r.second) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connLink = r.first, connChatUsed = false, conn = r.second) + contactConnection.value = r.second + } + } else { + creatingConnReq.value = false + if (alert != null) { + alert() + } + } + } + } + } + + DisposableEffect(Unit) { + onDispose { + if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) { + val conn = contactConnection.value + if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.keep_unused_invitation_question), + text = generalGetString(MR.strings.you_can_view_invitation_link_again), + confirmText = generalGetString(MR.strings.delete_verb), + dismissText = generalGetString(MR.strings.keep_invitation_link), + destructive = true, + onConfirm = { + withBGApi { + val chatInfo = ChatInfo.ContactConnection(conn) + controller.deleteChat(Chat(remoteHostId = rhId, chatInfo = chatInfo, chatItems = listOf())) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.start.closeModals() + } + } + } + ) + } + chatModel.showingInvitation.value = null + } + } + } + + ModalView(close) { + OneTimeLinkContent(connLinkInvitation) + } +} + +@Composable +fun OneTimeLinkContent(connLinkInvitation: CreatedConnLink) { + val showShortLink = remember { mutableStateOf(true) } + + ColumnWithScrollBar( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + + Image( + painterResource(MR.images.ic_invitation_one_time_link), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING * 3) + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + Text( + stringResource(MR.strings.send_1_time_link_description), + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(horizontal = DEFAULT_PADDING) + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + if (connLinkInvitation.connFullLink.isNotEmpty()) { + OneTimeLinkBar(connLinkInvitation, showShortLink.value) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + Text( + stringResource(MR.strings.or_show_qr_code_in_person_or_video_call), + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(horizontal = DEFAULT_PADDING) + ) + + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colors.background, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + ) { + SimpleXCreatedLinkQRCode( + connLinkInvitation, + short = showShortLink.value, + onShare = { chatModel.markShowingInvitationUsed() } + ) + } + } else { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + Modifier.size(36.dp).padding(4.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } + } + + SectionBottomSpacer() + } +} + +@Composable +private fun OneTimeLinkBar(connLinkInvitation: CreatedConnLink, short: Boolean) { + val clipboard = LocalClipboardManager.current + val link = connLinkInvitation.simplexChatUri(short) + + Surface( + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + ) { + Row( + Modifier + .padding(start = DEFAULT_PADDING, end = 4.dp) + .heightIn(min = 48.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + link, + style = MaterialTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = { + chatModel.markShowingInvitationUsed() + clipboard.setText(AnnotatedString(simplexChatLink(link))) + }) { + Icon( + painterResource(MR.images.ic_content_copy), + contentDescription = stringResource(MR.strings.copy_verb), + tint = MaterialTheme.colors.primary + ) + } + IconButton(onClick = { + chatModel.markShowingInvitationUsed() + clipboard.shareText(simplexChatLink(link)) + }) { + Icon( + painterResource(MR.images.ic_share), + contentDescription = stringResource(MR.strings.share_verb), + tint = MaterialTheme.colors.primary + ) + } + } + } +} + +@Preview +@Composable +fun PreviewOneTimeLinkView() { + SimpleXTheme { + OneTimeLinkContent( + connLinkInvitation = CreatedConnLink("https://smp16.simplex.im/i#pT0CA_nnqmLA", null) + ) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt index 6cf432a15f..bae72ec5a3 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt @@ -24,12 +24,14 @@ import boofcv.alg.color.ColorFormat import boofcv.android.ConvertCameraImage import boofcv.factory.fiducial.FactoryFiducial import boofcv.struct.image.GrayU8 +import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert import chat.simplex.common.platform.TAG import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale import com.google.common.util.concurrent.ListenableFuture import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -143,7 +145,18 @@ actual fun QRCodeScanner( } } cameraPermissionState.status is PermissionStatus.Denied -> { - Button({ cameraPermissionState.launchPermissionRequest() }, modifier = modifier, colors = buttonColors) { + val context = LocalContext.current + Button( + onClick = { + if (cameraPermissionState.status.shouldShowRationale) { + cameraPermissionState.launchPermissionRequest() + } else { + context.showAllowPermissionInSettingsAlert() + } + }, + modifier = modifier, + colors = buttonColors + ) { Icon(painterResource(MR.images.ic_camera_enhance), null) Spacer(Modifier.width(DEFAULT_PADDING_HALF)) Text(stringResource(MR.strings.enable_camera_access)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index dcb947e882..24c5e5535f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -292,7 +292,7 @@ private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState Unit) { ModalView(close) { ColumnWithScrollBar( - Modifier.fillMaxWidth(), + Modifier.fillMaxWidth().background(Color(0xfff5f5f6)), horizontalAlignment = Alignment.CenterHorizontally ) { - AppBarTitle(stringResource(MR.strings.connect_via_link), withPadding = false) + Spacer(Modifier.height(24.dp)) - Spacer(Modifier.height(DEFAULT_PADDING)) - - Icon( - painterResource(MR.images.ic_add_link), + Image( + painterResource(MR.images.ic_invitation_connect_link), contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colors.primary + contentScale = ContentScale.Fit, + modifier = Modifier + .size(200.dp) + .padding(horizontal = DEFAULT_PADDING) ) - Spacer(Modifier.height(DEFAULT_PADDING * 1.5f)) + Spacer(Modifier.height(24.dp)) SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { ConnectPasteLinkView(rhId, pastedLink, showQRCodeScanner, close) @@ -61,7 +66,12 @@ fun ModalData.ConnectViewLinkOrQrModal(rhId: Long?, close: () -> Unit) { Spacer(Modifier.height(10.dp)) SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) { - Box(Modifier.clip(RoundedCornerShape(DEFAULT_PADDING))) { + Box( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + .clip(RoundedCornerShape(24.dp)) + ) { QRCodeScanner(showQRCodeScanner) { text -> val linkVerified = strIsSimplexLink(text) if (!linkVerified) { @@ -101,11 +111,30 @@ private fun ConnectPasteLinkView(rhId: Long?, pastedLink: MutableState, ) } }) { - Box(Modifier.weight(1f)) { - Text(stringResource(MR.strings.tap_to_paste_link)) - } - if (connectProgressManager.showConnectProgress != null) { - CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) + Box( + Modifier + .weight(1f) + .background(Color(0xFFF2F2F7)) // light gray outer background + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + Box( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(MR.strings.tap_to_paste_link), + modifier = Modifier.padding(vertical = 16.dp), + color = Color.Black.copy(alpha = 0.3f), + fontSize = 18.sp + ) + if (connectProgressManager.showConnectProgress != null) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) + } + } } } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/EmptyChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/EmptyChatListView.kt index b39d3b1b89..283682535e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/EmptyChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/EmptyChatListView.kt @@ -1,16 +1,15 @@ package chat.simplex.common.views.invitation_redesign +import SectionBottomSpacer import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.platform.* @@ -20,88 +19,62 @@ import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @Composable -fun BoxScope.EmptyChatListView() { - val closeAll = { ModalManager.start.closeModals() } - Column( - Modifier - .align(Alignment.Center) - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - stringResource(MR.strings.now_you_can), - style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Bold), - ) - Spacer(Modifier.height(DEFAULT_PADDING)) - Surface( - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.appColors.sentMessage, - modifier = Modifier.fillMaxWidth().clickable { - ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> - NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) - } - } +fun BoxScope.NowYouCanView() { + var showInviteSomeone by remember { mutableStateOf(false) } + + if (showInviteSomeone) { + BackHandler { showInviteSomeone = false } + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - Modifier.fillMaxWidth().padding(DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painterResource(MR.images.ic_invitation_card_invite_someone), - contentDescription = null, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painterResource(MR.images.ic_repeat_one), - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colors.secondary - ) - Spacer(Modifier.width(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.invite_someone_to_chat), - style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium), - ) - } + Row(Modifier.fillMaxWidth()) { + NavigationButtonBack(onButtonClicked = { showInviteSomeone = false }) } + Text( + stringResource(MR.strings.invite_someone), + style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Bold), + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + SectionBottomSpacer() } - Spacer(Modifier.height(DEFAULT_PADDING)) - Surface( - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.appColors.sentMessage, - modifier = Modifier.fillMaxWidth().clickable { - ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> - NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) - } - } + } else { + val closeAll = { ModalManager.start.closeModals() } + Column( + Modifier + .align(Alignment.Center) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - Modifier.fillMaxWidth().padding(DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painterResource(MR.images.ic_invitation_card_one_time_link), - contentDescription = null, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painterResource(MR.images.ic_qr_code), - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colors.secondary - ) - Spacer(Modifier.width(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.connect_via_link_or_qr), - style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium), - ) + Text( + stringResource(MR.strings.now_you_can), + style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Bold), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING) + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + InviteCardComponent( + image = painterResource(MR.images.ic_invitation_card_invite_someone), + titleIcon = painterResource(MR.images.ic_repeat_one), + title = stringResource(MR.strings.invite_someone_to_chat), + onClick = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + InviteSomeoneView {} + } } - } + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + InviteCardComponent( + image = painterResource(MR.images.ic_invitation_card_one_time_link), + titleIcon = painterResource(MR.images.ic_qr_code), + title = stringResource(MR.strings.connect_via_link_or_qr), + onClick = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + ConnectViewLinkOrQrModal(chatModel.currentRemoteHost.value?.remoteHostId, {}) + } + } + ) } } } @@ -111,7 +84,7 @@ fun BoxScope.EmptyChatListView() { fun PreviewEmptyChatListView() { SimpleXTheme { Box(Modifier.fillMaxSize()) { - EmptyChatListView() + NowYouCanView() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteCardComponent.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteCardComponent.kt new file mode 100644 index 0000000000..412cfebed8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteCardComponent.kt @@ -0,0 +1,94 @@ +package chat.simplex.common.views.invitation_redesign + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.ui.theme.DEFAULT_PADDING + +@Composable +fun InviteCardComponent( + image: Painter, + titleIcon: Painter, + title: String, + description: String? = null, + onClick: () -> Unit +) { + Card( + shape = RoundedCornerShape(24.dp), + backgroundColor = Color(0xfff5f5f6), + elevation = 0.dp, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + .clickable(onClick = onClick) + ) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + image, + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + Icon( + titleIcon, + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(Modifier.width(8.dp)) + Text( + title, + style = MaterialTheme.typography.body1.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ), + color = Color.Black + ) + } + if (description != null) { + Text( + description, + style = MaterialTheme.typography.body1.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ), + color = Color.Black + ) + Spacer(Modifier.height(8.dp)) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteSomeoneView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteSomeoneView.kt index cf0ce8df94..606df87f64 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteSomeoneView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/InviteSomeoneView.kt @@ -2,6 +2,8 @@ package chat.simplex.common.views.invitation_redesign import SectionBottomSpacer import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -9,143 +11,75 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.* import chat.simplex.common.views.usersettings.UserAddressView import chat.simplex.res.MR @Composable -fun ModalData.InviteSomeoneView(close: () -> Unit) { - val closeAll = { ModalManager.start.closeModals() } - +fun InviteSomeoneView(close: () -> Unit) { ModalView(close) { ColumnWithScrollBar( - Modifier.fillMaxWidth(), + Modifier.fillMaxSize().background(Color.White), horizontalAlignment = Alignment.CenterHorizontally ) { AppBarTitle(stringResource(MR.strings.invite_someone), withPadding = false) - Spacer(Modifier.height(DEFAULT_PADDING)) - - Surface( - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.appColors.sentMessage, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING) - .clickable { - ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> - NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) - } - } - ) { - Column( - Modifier.fillMaxWidth().padding(DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - painterResource(MR.images.ic_add_link), - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colors.primary - ) - } - } - - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - - Row( - Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painterResource(MR.images.ic_repeat_one), - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.secondary - ) - Spacer(Modifier.width(DEFAULT_PADDING_HALF)) - Column { - Text( - stringResource(MR.strings.create_private_1_time_link), - style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold), - ) - Text( - stringResource(MR.strings.contact_can_use_link_or_scan_qr), - style = MaterialTheme.typography.body2, - color = MaterialTheme.colors.secondary - ) - } - } - - Spacer(Modifier.height(DEFAULT_PADDING * 1.5f)) - - Surface( - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.appColors.sentMessage, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING) - .clickable { - ModalManager.start.showModalCloseable { closeAddress -> - UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = closeAddress) - } - } - ) { - Column( - Modifier.fillMaxWidth().padding(DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - painterResource(MR.images.ic_qr_code), - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colors.primary - ) - } - } - - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - - Row( - Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painterResource(MR.images.ic_qr_code), - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.secondary - ) - Spacer(Modifier.width(DEFAULT_PADDING_HALF)) - Column { - Text( - stringResource(MR.strings.create_public_simplex_address), - style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold), - ) - Text( - stringResource(MR.strings.public_link_for_social_media_email_or_website), - style = MaterialTheme.typography.body2, - color = MaterialTheme.colors.secondary - ) - } - } - + InviteSomeoneContent() SectionBottomSpacer() } } } +@Composable +fun InviteSomeoneContent() { + InviteCardComponent( + image = painterResource(MR.images.ic_invitation_card_one_time_link), + titleIcon = painterResource(MR.images.ic_add_link), + title = stringResource(MR.strings.create_private_1_time_link), + description = stringResource(MR.strings.contact_can_use_link_or_scan_qr), + onClick = { + ModalManager.start.showModalCloseable { close -> + OneTimeLinkView(rhId = chatModel.currentRemoteHost.value?.remoteHostId, close = close) + } + } + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + InviteCardComponent( + image = painterResource(MR.images.ic_invitation_card_public_address), + titleIcon = painterResource(MR.images.ic_qr_code), + title = stringResource(MR.strings.create_public_simplex_address), + description = stringResource(MR.strings.public_link_for_social_media_email_or_website), + onClick = { + ModalManager.start.showModalCloseable { close -> + OneTimeLinkView(rhId = chatModel.currentRemoteHost.value?.remoteHostId, close = close) + } + } + ) +} + @Preview @Composable fun PreviewInviteSomeoneView() { SimpleXTheme { - ModalData().InviteSomeoneView(close = {}) + ColumnWithScrollBar( + Modifier.fillMaxSize().background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppBarTitle(stringResource(MR.strings.invite_someone), withPadding = false) + Spacer(Modifier.height(DEFAULT_PADDING)) + InviteSomeoneContent() + SectionBottomSpacer() + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/OneTimeLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/OneTimeLinkView.kt new file mode 100644 index 0000000000..ca5d7b80a8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/OneTimeLinkView.kt @@ -0,0 +1,219 @@ +package chat.simplex.common.views.invitation_redesign + +import SectionBottomSpacer +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +@Composable +fun OneTimeLinkView(rhId: Long?, close: () -> Unit) { + val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } + val connLinkInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connLink ?: CreatedConnLink("", null) } } + val creatingConnReq = rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if ( + connLinkInvitation.connFullLink.isEmpty() + && contactConnection.value == null + && !creatingConnReq.value + ) { + creatingConnReq.value = true + withBGApi { + val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, r.second) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connLink = r.first, connChatUsed = false, conn = r.second) + contactConnection.value = r.second + } + } else { + creatingConnReq.value = false + if (alert != null) { + alert() + } + } + } + } + } + + DisposableEffect(Unit) { + onDispose { + if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) { + val conn = contactConnection.value + if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.keep_unused_invitation_question), + text = generalGetString(MR.strings.you_can_view_invitation_link_again), + confirmText = generalGetString(MR.strings.delete_verb), + dismissText = generalGetString(MR.strings.keep_invitation_link), + destructive = true, + onConfirm = { + withBGApi { + val chatInfo = ChatInfo.ContactConnection(conn) + controller.deleteChat(Chat(remoteHostId = rhId, chatInfo = chatInfo, chatItems = listOf())) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.start.closeModals() + } + } + } + ) + } + chatModel.showingInvitation.value = null + } + } + } + + ModalView(close) { + OneTimeLinkContent(connLinkInvitation) + } +} + +@Composable +fun OneTimeLinkContent(connLinkInvitation: CreatedConnLink) { + val showShortLink = remember { mutableStateOf(true) } + + ColumnWithScrollBar( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + + Image( + painterResource(MR.images.ic_invitation_one_time_link), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING * 3) + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + Text( + stringResource(MR.strings.send_1_time_link_description), + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(horizontal = DEFAULT_PADDING) + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + if (connLinkInvitation.connFullLink.isNotEmpty()) { + OneTimeLinkBar(connLinkInvitation, showShortLink.value) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + Text( + stringResource(MR.strings.or_show_qr_code_in_person_or_video_call), + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(horizontal = DEFAULT_PADDING) + ) + + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colors.background, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + ) { + SimpleXCreatedLinkQRCode( + connLinkInvitation, + short = showShortLink.value, + onShare = { chatModel.markShowingInvitationUsed() } + ) + } + } else { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + Modifier.size(36.dp).padding(4.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } + } + + SectionBottomSpacer() + } +} + +@Composable +private fun OneTimeLinkBar(connLinkInvitation: CreatedConnLink, short: Boolean) { + val clipboard = LocalClipboardManager.current + val link = connLinkInvitation.simplexChatUri(short) + + Surface( + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.appColors.sentMessage, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + ) { + Row( + Modifier + .padding(start = DEFAULT_PADDING, end = 4.dp) + .heightIn(min = 48.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + link, + style = MaterialTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = { + chatModel.markShowingInvitationUsed() + clipboard.setText(AnnotatedString(simplexChatLink(link))) + }) { + Icon( + painterResource(MR.images.ic_content_copy), + contentDescription = stringResource(MR.strings.copy_verb), + tint = MaterialTheme.colors.primary + ) + } + IconButton(onClick = { + chatModel.markShowingInvitationUsed() + clipboard.shareText(simplexChatLink(link)) + }) { + Icon( + painterResource(MR.images.ic_share), + contentDescription = stringResource(MR.strings.share_verb), + tint = MaterialTheme.colors.primary + ) + } + } + } +} + +@Preview +@Composable +fun PreviewOneTimeLinkView() { + SimpleXTheme { + OneTimeLinkContent( + connLinkInvitation = CreatedConnLink("https://smp16.simplex.im/i#pT0CA_nnqmLA", null) + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index e8084e055a..5bc7a47af2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -83,26 +83,31 @@ fun AddGroupLayout( val focusRequester = remember { FocusRequester() } val incognito = remember { mutableStateOf(incognitoPref.get()) } - ModalBottomSheetLayout( - scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.imePadding(), - sheetContent = { - GetImageBottomSheet( - chosenImage, - onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) }, - hideBottomSheet = { - scope.launch { bottomSheetModalState.hide() } - }) - }, - sheetState = bottomSheetModalState, - sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) - ) { - ModalView(close = close) { - ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) + ModalBottomSheetLayout( + scrimColor = Color.Black.copy(alpha = 0.12F), + modifier = Modifier.imePadding(), + sheetContent = { + GetImageBottomSheet( + chosenImage, + onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) }, + hideBottomSheet = { + scope.launch { bottomSheetModalState.hide() } + }) + }, + sheetState = bottomSheetModalState, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) { + ModalView(close = close) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING * 3), + verticalAlignment = Alignment.CenterVertically + ) { Box( Modifier - .fillMaxWidth() + .weight(1f) .padding(bottom = 24.dp), contentAlignment = Alignment.Center ) { @@ -116,57 +121,67 @@ fun AddGroupLayout( } } } - Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - stringResource(MR.strings.group_display_name_field), - fontSize = 16.sp - ) - if (!isValidDisplayName(displayName.value.trim())) { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - IconButton({ showInvalidNameAlert(mkValidName(displayName.value.trim()), displayName) }, Modifier.size(20.dp)) { - Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) - } + + Image( + painterResource(MR.images.ic_invitation_create_group), + contentDescription = null, + modifier = Modifier.weight(1f) + ) + } + + Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.group_display_name_field), + fontSize = 16.sp + ) + if (!isValidDisplayName(displayName.value.trim())) { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value.trim()), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) } } - Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { - ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) - } - Spacer(Modifier.height(8.dp)) + } + Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { + ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + } + Spacer(Modifier.height(8.dp)) - SettingsActionItem( - painterResource(MR.images.ic_check), - stringResource(MR.strings.create_group_button), - click = { - createGroup(incognito.value, GroupProfile( + SettingsActionItem( + painterResource(MR.images.ic_check), + stringResource(MR.strings.create_group_button), + click = { + createGroup( + incognito.value, GroupProfile( displayName = displayName.value.trim(), fullName = "", shortDescr = null, image = profileImage.value, groupPreferences = GroupPreferences(history = GroupPreference(GroupFeatureEnabled.ON)) - )) - }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - disabled = !canCreateProfile(displayName.value) - ) + ) + ) + }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + disabled = !canCreateProfile(displayName.value) + ) - IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } + IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } - SectionTextFooter( - buildAnnotatedString { - append(sharedProfileInfo(chatModel, incognito.value)) - append("\n") - append(annotatedStringResource(MR.strings.group_is_decentralized)) - } - ) - - LaunchedEffect(Unit) { - delay(1000) - focusRequester.requestFocus() + SectionTextFooter( + buildAnnotatedString { + append(sharedProfileInfo(chatModel, incognito.value)) + append("\n") + append(annotatedStringResource(MR.strings.group_is_decentralized)) } + ) + + LaunchedEffect(Unit) { + delay(1000) + focusRequester.requestFocus() } } } + } } fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) 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 f520a86999..d7374608db 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 @@ -24,6 +24,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -452,6 +453,16 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact val showShortLink = remember { mutableStateOf(true) } Spacer(Modifier.height(10.dp)) + Image( + painterResource(MR.images.ic_invitation_one_time_link), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING * 3) + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true) } @@ -584,6 +595,16 @@ private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, p } } + Image( + painterResource(MR.images.ic_invitation_connect_link), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING * 3) + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { PasteLinkView(rhId, pastedLink, showQRCodeScanner, close) } @@ -681,6 +702,13 @@ fun LinkTextView(link: String, share: Boolean) { // So using BasicTextField + manual ... Text("…", fontSize = 16.sp) if (share) { + Spacer(Modifier.width(DEFAULT_PADDING)) + IconButton({ + chatModel.markShowingInvitationUsed() + clipboard.setText(AnnotatedString(simplexChatLink(link))) + }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_content_copy), stringResource(MR.strings.copy_verb), tint = MaterialTheme.colors.primary) + } Spacer(Modifier.width(DEFAULT_PADDING)) IconButton({ chatModel.markShowingInvitationUsed() 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 49b388adf1..47a2284590 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -467,6 +467,8 @@ Invite someone Create private 1-time link Your contact can use link or scan QR code + Send 1-time link to your contact via any messenger, it is secure. Ask to use it in the app. + Or show this QR code in person or in a video call. Create public SimpleX address Public link for social media, email or website Loading chats… diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_invitation_one_time_link@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_invitation_one_time_link@4x.png index f9d7bf8b0a..db236014e7 100644 Binary files a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_invitation_one_time_link@4x.png and b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_invitation_one_time_link@4x.png differ