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 f4e3b62861..3b02db61e1 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 @@ -63,6 +63,9 @@ class ChatModel(val controller: ChatController) { val showCallView = mutableStateOf(false) val switchingCall = mutableStateOf(false) + // currently showing QR code + val connReqInv = mutableStateOf(null as String?) + // working with external intents val sharedContent = mutableStateOf(null as SharedContent?) @@ -283,6 +286,14 @@ class ChatModel(val controller: ChatController) { chats.add(index = 0, chat) } + fun dismissConnReqView(id: String) { + if (connReqInv.value == null) return + val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return + if (info.contactConnection.connReqInv == connReqInv.value) { + ModalManager.shared.closeModals() + } + } + fun removeChat(id: String) { chats.removeAll { it.id == id } } @@ -878,6 +889,8 @@ class PendingContactConnection( val pccConnStatus: ConnStatus, val viaContactUri: Boolean, val customUserProfileId: Long? = null, + val connReqInv: String? = null, + override val localAlias: String, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -902,7 +915,6 @@ class PendingContactConnection( } override val fullName get() = "" override val image get() = null - override val localAlias get() = "" val initiated get() = (pccConnStatus.initiated ?: false) && !viaContactUri @@ -927,6 +939,7 @@ class PendingContactConnection( pccAgentConnId = "abcd", pccConnStatus = status, viaContactUri = viaContactUri, + localAlias = "", customUserProfileId = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now() 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 9c556cfa1a..f46b8b754b 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 @@ -597,6 +597,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } + suspend fun apiSetConnectionAlias(connId: Long, localAlias: String): PendingContactConnection? { + val r = sendCmd(CC.ApiSetConnectionAlias(connId, localAlias)) + if (r is CR.ConnectionAliasUpdated) return r.toConnection + Log.e(TAG, "apiSetConnectionAlias bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiCreateUserAddress(): String? { val r = sendCmd(CC.CreateMyAddress()) return when (r) { @@ -846,12 +853,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } is CR.ContactConnected -> { chatModel.updateContact(r.contact) + chatModel.dismissConnReqView(r.contact.activeConn.id) chatModel.removeChat(r.contact.activeConn.id) chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected()) ntfManager.notifyContactConnected(r.contact) } is CR.ContactConnecting -> { chatModel.updateContact(r.contact) + chatModel.dismissConnReqView(r.contact.activeConn.id) chatModel.removeChat(r.contact.activeConn.id) } is CR.ReceivedContactRequest -> { @@ -1340,6 +1349,7 @@ sealed class CC { class ApiUpdateProfile(val profile: Profile): CC() class ApiParseMarkdown(val text: String): CC() class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() + class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() class CreateMyAddress: CC() class DeleteMyAddress: CC() class ShowMyAddress: CC() @@ -1394,6 +1404,7 @@ sealed class CC { is ApiUpdateProfile -> "/_profile ${json.encodeToString(profile)}" is ApiParseMarkdown -> "/_parse $text" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" + is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" is CreateMyAddress -> "/address" is DeleteMyAddress -> "/delete_address" is ShowMyAddress -> "/show_address" @@ -1449,6 +1460,7 @@ sealed class CC { is ApiUpdateProfile -> "updateProfile" is ApiParseMarkdown -> "apiParseMarkdown" is ApiSetContactAlias -> "apiSetContactAlias" + is ApiSetConnectionAlias -> "apiSetConnectionAlias" is CreateMyAddress -> "createMyAddress" is DeleteMyAddress -> "deleteMyAddress" is ShowMyAddress -> "showMyAddress" @@ -1637,6 +1649,7 @@ sealed class CR { @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR() @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR() + @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR() @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() @Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR() @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR() @@ -1725,6 +1738,7 @@ sealed class CR { is UserProfileNoChange -> "userProfileNoChange" is UserProfileUpdated -> "userProfileUpdated" is ContactAliasUpdated -> "contactAliasUpdated" + is ConnectionAliasUpdated -> "connectionAliasUpdated" is ParsedMarkdown -> "apiParsedMarkdown" is UserContactLink -> "userContactLink" is UserContactLinkCreated -> "userContactLinkCreated" @@ -1811,6 +1825,7 @@ sealed class CR { is UserProfileNoChange -> noDetails() is UserProfileUpdated -> json.encodeToString(toProfile) is ContactAliasUpdated -> json.encodeToString(toContact) + is ConnectionAliasUpdated -> json.encodeToString(toConnection) is ParsedMarkdown -> json.encodeToString(formattedText) is UserContactLink -> connReqContact is UserContactLinkCreated -> connReqContact diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index b07a436d00..1e3b7f0d2e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -208,7 +208,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { } @Composable -private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) { +fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) { var value by rememberSaveable { mutableStateOf(initialValue) } Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { DefaultBasicTextField( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 464e851a5c..1173528854 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -63,7 +63,10 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { is ChatInfo.ContactConnection -> ChatListNavLinkLayout( chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) }, - click = { contactConnectionAlertDialog(chat.chatInfo.contactConnection, chatModel) }, + click = { chat.chatInfo.contactConnection.connReqInv.let { + if (it == null) contactConnectionAlertDialog(chat.chatInfo.contactConnection, chatModel) + else ModalManager.shared.showModalCloseable { close -> ContactConnectionInfoView(chatModel, it, chat.chatInfo.contactConnection, close) } + } }, dropdownMenuItems = { ContactConnectionMenuItems(chat.chatInfo, chatModel, showMenu) }, showMenu, stopped @@ -268,7 +271,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: stringResource(R.string.delete_verb), Icons.Outlined.Delete, onClick = { - deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) + deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {} showMenu.value = false }, color = Color.Red @@ -337,7 +340,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel ) { TextButton(onClick = { AlertManager.shared.hideAlert() - deleteContactConnectionAlert(connection, chatModel) + deleteContactConnectionAlert(connection, chatModel) {} }) { Text(stringResource(R.string.delete_verb)) } @@ -350,7 +353,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel ) } -fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel) { +fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(R.string.delete_pending_connection__question), text = generalGetString( @@ -363,6 +366,7 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel AlertManager.shared.hideAlert() if (chatModel.controller.apiDeleteChat(ChatType.ContactConnection, connection.apiId)) { chatModel.removeChat(connection.id) + onSuccess() } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt index 2ee852aeb3..87dbef9cf2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt @@ -4,16 +4,14 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AddLink -import androidx.compose.material.icons.outlined.Link -import androidx.compose.runtime.Composable +import androidx.compose.material.icons.outlined.* +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.text.style.TextOverflow import androidx.compose.ui.unit.dp -import chat.simplex.app.model.PendingContactConnection -import chat.simplex.app.model.getTimestampText +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.ProfileImage @@ -21,7 +19,16 @@ import chat.simplex.app.views.helpers.ProfileImage fun ContactConnectionView(contactConnection: PendingContactConnection) { Row { Box(Modifier.size(72.dp), contentAlignment = Alignment.Center) { - ProfileImage(size = 54.dp, null, if (contactConnection.initiated) Icons.Outlined.AddLink else Icons.Outlined.Link) + ProfileImage( + size = 54.dp, + null, + if (contactConnection.initiated) Icons.Outlined.QrCode else Icons.Outlined.Link, + color = if (contactConnection.connReqInv == null) + MaterialTheme.colors.secondary + else if (contactConnection.incognito) + Indigo else + MaterialTheme.colors.primary + ) } Column( modifier = Modifier diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index 36f72c269d..ae184c1efe 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -63,7 +63,7 @@ class ModalManager { } fun closeModals() { - while (modalViews.isNotEmpty()) closeModal() + while (modalCount.value > 0) closeModal() } @OptIn(ExperimentalAnimationApi::class) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt new file mode 100644 index 0000000000..20f831a6fd --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt @@ -0,0 +1,142 @@ +package chat.simplex.app.views.chatlist + +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.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.chat.LocalAliasEditor +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.newchat.InfoAboutIncognito +import chat.simplex.app.views.newchat.QRCode + +@Composable +fun ContactConnectionInfoView(chatModel: ChatModel, connReqInvitation: String, contactConnection: PendingContactConnection, close: () -> Unit) { + val cxt = LocalContext.current + LaunchedEffect(Unit) { + if (contactConnection.connReqInv != null) { + chatModel.connReqInv.value = contactConnection.connReqInv + } + } + DisposableEffect(Unit) { + onDispose { chatModel.connReqInv.value = null } + } + ContactConnectionInfoLayout( + chatModelIncognito = chatModel.incognito.value, + connReq = connReqInvitation, + contactConnection.localAlias, + share = { shareText(cxt, connReqInvitation) }, + deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) }, + onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) } + ) +} + +@Composable +private fun ContactConnectionInfoLayout( + chatModelIncognito: Boolean, + connReq: String, + localAlias: String, + share: () -> Unit, + deleteConnection: () -> Unit, + onLocalAliasChanged: (String) -> Unit +) { + BoxWithConstraints { + val screenHeight = maxHeight + var showQr by remember { mutableStateOf(false) } + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING , bottom = DEFAULT_PADDING), + verticalArrangement = Arrangement.SpaceBetween, + ) { + AppBarTitle(stringResource(R.string.shared_one_time_link), false) + + LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged) + + Text( + stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline), + ) + Row { + InfoAboutIncognito( + chatModelIncognito, + true, + generalGetString(R.string.incognito_random_profile_description), + generalGetString(R.string.your_profile_will_be_sent) + ) + } + if (connReq.isNotEmpty() && showQr) { + QRCode( + connReq, Modifier + .aspectRatio(1f) + .padding(vertical = 3.dp) + ) + + Text( + annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel), + lineHeight = 22.sp, + modifier = Modifier + .padding(bottom = if (screenHeight > 600.dp) 8.dp else 0.dp) + ) + } + if (!showQr) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + SimpleButton(stringResource(R.string.show_QR_code), icon = Icons.Outlined.QrCode, click = { showQr = true }) + } + } + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share) + } + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + SimpleButton(stringResource(R.string.delete_verb), icon = Icons.Outlined.Delete, color = Color.Red, click = deleteConnection) + } + } + } +} + +private fun setContactAlias(contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi { + chatModel.controller.apiSetConnectionAlias(contactConnection.pccConnId, localAlias)?.let { + chatModel.updateContactConnection(it) + } +} + +@Preview +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +private fun PreviewAddContactView() { + SimpleXTheme { + ContactConnectionInfoLayout( + chatModelIncognito = false, + localAlias = "", + connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", + share = {}, + deleteConnection = {}, + onLocalAliasChanged = {}, + ) + } +} 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 index c0042faca8..1fe555a85a 100644 --- 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 @@ -29,6 +29,14 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { createInvitation(m, creatingConnReq, connReqInvitation) } } + LaunchedEffect(connReqInvitation.value) { + if (connReqInvitation.value.isNotEmpty()) { + m.connReqInv.value = connReqInvitation.value + } + } + DisposableEffect(Unit) { + onDispose { m.connReqInv.value = null } + } val tabTitles = CreateLinkTab.values().map { when { it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link) diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index 472525d67e..4f96cea009 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -307,6 +307,10 @@ E-Mail Mehr + + Show QR code + Shared one-time link + Ungültiger QR-Code Dieser QR-Code beschreibt keinen Link! 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 f12840a1c4..e460789e4b 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -307,6 +307,10 @@ Email Больше + + Показать QR код + Поделиться одноразовой ссылкой + Неверный QR код Этот QR код не является ссылкой! diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index f3425051dc..d8f214435c 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -307,6 +307,10 @@ Email More + + Show QR code + Shared one-time link + Invalid QR code This QR code is not a link! @@ -315,7 +319,7 @@ 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! - Your contact can scan it from the app. + Your contact can scan QR code 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.