diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6fe1bef8e..bad15ad52 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -594,7 +594,6 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P } func apiConnectPlan(connReq: String) async throws -> ConnectionPlan { - logger.error("apiConnectPlan connReq: \(connReq)") let userId = try currentUserId("apiConnectPlan") let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq)) if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan } diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index 249180686..935f09cc1 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -194,7 +194,7 @@ struct SendAddressMailView: View { let messageBody = String(format: NSLocalizedString("""

Hi!

Connect to me via SimpleX Chat

- """, comment: "email text"), userAddress.connReqContact) + """, comment: "email text"), simplexChatLink(userAddress.connReqContact)) MailView( isShowing: self.$showMailView, result: $mailViewResult, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 132384219..ad033387a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -136,6 +136,7 @@ object ChatModel { fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id } fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } + fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id } fun addChat(chat: Chat) = chats.add(index = 0, chat) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 6bd269f24..8397d2edb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -855,6 +855,14 @@ object ChatController { return null } + suspend fun apiConnectPlan(connReq: String): ConnectionPlan? { + val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } + val r = sendCmd(CC.APIConnectPlan(userId, connReq)) + if (r is CR.CRConnectionPlan) return r.connectionPlan + Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiConnect(incognito: Boolean, connReq: String): Boolean { val userId = chatModel.currentUser.value?.userId ?: run { Log.e(TAG, "apiConnect: no current user") @@ -1914,6 +1922,7 @@ sealed class CC { class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() + class APIConnectPlan(val userId: Long, val connReq: String): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() class ApiDeleteChat(val type: ChatType, val id: Long): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() @@ -2023,6 +2032,7 @@ sealed class CC { is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" + is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" @@ -2124,6 +2134,7 @@ sealed class CC { is APIVerifyGroupMember -> "apiVerifyGroupMember" is APIAddContact -> "apiAddContact" is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" + is APIConnectPlan -> "apiConnectPlan" is APIConnect -> "apiConnect" is ApiDeleteChat -> "apiDeleteChat" is ApiClearChat -> "apiClearChat" @@ -3337,6 +3348,7 @@ sealed class CR { @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR() @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() + @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR() @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() @@ -3472,6 +3484,7 @@ sealed class CR { is ConnectionVerified -> "connectionVerified" is Invitation -> "invitation" is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" + is CRConnectionPlan -> "connectionPlan" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" is ContactAlreadyExists -> "contactAlreadyExists" @@ -3602,6 +3615,7 @@ sealed class CR { is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") is Invitation -> withUser(user, connReqInvitation) is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) + is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan)) is SentConfirmation -> withUser(user, noDetails()) is SentInvitation -> withUser(user, noDetails()) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) @@ -3715,6 +3729,39 @@ fun chatError(r: CR): ChatErrorType? { ) } +@Serializable +sealed class ConnectionPlan { + @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() + @Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() + @Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() +} + +@Serializable +sealed class InvitationLinkPlan { + @Serializable @SerialName("ok") object Ok: InvitationLinkPlan() + @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan() + @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan() + @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan() +} + +@Serializable +sealed class ContactAddressPlan { + @Serializable @SerialName("ok") object Ok: ContactAddressPlan() + @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() + @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() + @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() + @Serializable @SerialName("known") class Known(val contact: Contact): ContactAddressPlan() +} + +@Serializable +sealed class GroupLinkPlan { + @Serializable @SerialName("ok") object Ok: GroupLinkPlan() + @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() + @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() + @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() + @Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan() +} + abstract class TerminalItem { abstract val id: Long val date: Instant = Clock.System.now() @@ -3874,6 +3921,7 @@ sealed class ChatErrorType { is ChatNotStarted -> "chatNotStarted" is ChatNotStopped -> "chatNotStopped" is ChatStoreChanged -> "chatStoreChanged" + is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" is InvalidChatMessage -> "invalidChatMessage" is ContactNotReady -> "contactNotReady" @@ -3950,6 +3998,7 @@ sealed class ChatErrorType { @Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType() @Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType() @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() + @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 1ba742b03..01173157a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -31,10 +31,10 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.updateChatSettings +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -309,9 +309,9 @@ fun ChatInfoLayout( if (contact.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + SimpleXLinkQRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) val clipboard = LocalClipboardManager.current - ShareAddressButton { clipboard.shareText(contact.contactLink) } + ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) } SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) } SectionDividerSpaced() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 7c767f9b7..7e1c03130 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -19,7 +19,7 @@ import chat.simplex.common.model.* import chat.simplex.common.platform.shareText import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @Composable @@ -44,14 +44,12 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St createLink() } } - val clipboard = LocalClipboardManager.current GroupLinkLayout( groupLink = groupLink, groupInfo, groupLinkMemberRole, creatingLink, createLink = ::createLink, - share = { clipboard.shareText(groupLink ?: return@GroupLinkLayout) }, updateLink = { val role = groupLinkMemberRole.value if (role != null) { @@ -95,7 +93,6 @@ fun GroupLinkLayout( groupLinkMemberRole: MutableState, creatingLink: Boolean, createLink: () -> Unit, - share: () -> Unit, updateLink: () -> Unit, deleteLink: () -> Unit ) { @@ -125,16 +122,17 @@ fun GroupLinkLayout( } initialLaunch = false } - QRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING)) + SimpleXLinkQRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp) ) { + val clipboard = LocalClipboardManager.current SimpleButton( stringResource(MR.strings.share_link), icon = painterResource(MR.images.ic_share), - click = share + click = { clipboard.shareText(simplexChatLink(groupLink)) } ) SimpleButton( stringResource(MR.strings.delete_link), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index c76fb73a6..e281c5762 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -284,9 +284,9 @@ fun GroupMemberInfoLayout( if (member.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + SimpleXLinkQRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) val clipboard = LocalClipboardManager.current - ShareAddressButton { clipboard.shareText(member.contactLink) } + ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) } if (contactId != null) { if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) { ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) }) @@ -473,43 +473,17 @@ private fun updateMemberRoleDialog( } fun connectViaMemberAddressAlert(connReqUri: String) { - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.connect_via_member_address_alert_title), - text = AnnotatedString(generalGetString(MR.strings.connect_via_member_address_alert_desc)), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - withApi { - Log.d(TAG, "connectViaUri: connecting") - connectViaUri(chatModel, linkType, uri, incognito = false) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - withApi { - Log.d(TAG, "connectViaUri: connecting incognito") - connectViaUri(chatModel, linkType, uri, incognito = true) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } + try { + val uri = URI(connReqUri) + withApi { + planAndConnect(chatModel, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) } - ) + } catch (e: RuntimeException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_connection_link), + text = generalGetString(MR.strings.this_string_is_not_a_connection_link) + ) + } } @Preview diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index b45543df4..b16bf4c9b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -130,6 +130,13 @@ suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) { } } +suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) { + val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId) + if (chat != null) { + openChat(chat, chatModel) + } +} + suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}") val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId) 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 6d7450a21..66ef1cf9f 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 @@ -316,46 +316,8 @@ fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) { if (chatModel.currentUser.value == null) { chatModel.appOpenUrl.value = uri } else { - withUriAction(uri) { linkType -> - val title = when (linkType) { - ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link) - ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link) - ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link) - } - AlertManager.shared.showAlertDialogButtonsColumn( - title = title, - text = if (linkType == ConnectionLinkType.GROUP) - AnnotatedString(generalGetString(MR.strings.you_will_join_group)) - else - AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - Log.d(TAG, "connectIfOpenedViaUri: connecting") - connectViaUri(chatModel, linkType, uri, incognito = false) - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - Log.d(TAG, "connectIfOpenedViaUri: connecting incognito") - connectViaUri(chatModel, linkType, uri, incognito = true) - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - ) + withApi { + planAndConnect(chatModel, uri, incognito = null, close = null) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt index 8542ea52a..ef3633d1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt @@ -34,7 +34,6 @@ fun AddContactView( incognitoPref = chatModel.controller.appPrefs.incognito, connReq = connReqInvitation, contactConnection = contactConnection, - share = { clipboard.shareText(connReqInvitation) }, learnMore = { ModalManager.center.showModal { Column( @@ -56,7 +55,6 @@ fun AddContactLayout( incognitoPref: SharedPreference, connReq: String, contactConnection: MutableState, - share: () -> Unit, learnMore: () -> Unit ) { val incognito = remember { mutableStateOf(incognitoPref.get()) } @@ -82,7 +80,7 @@ fun AddContactLayout( SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) { if (connReq.isNotEmpty()) { - QRCode( + SimpleXLinkQRCode( connReq, Modifier .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) .aspectRatio(1f) @@ -99,7 +97,7 @@ fun AddContactLayout( } IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } - ShareLinkButton(share) + ShareLinkButton(connReq) OneTimeLinkLearnMoreButton(learnMore) } SectionTextFooter(sharedProfileInfo(chatModel, incognito.value)) @@ -109,11 +107,12 @@ fun AddContactLayout( } @Composable -fun ShareLinkButton(onClick: () -> Unit) { +fun ShareLinkButton(connReqInvitation: String) { + val clipboard = LocalClipboardManager.current SettingsActionItem( painterResource(MR.images.ic_share), stringResource(MR.strings.share_invitation_link), - onClick, + click = { clipboard.shareText(simplexChatLink(connReqInvitation)) }, iconColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary, ) @@ -177,7 +176,6 @@ fun PreviewAddContactView() { incognitoPref = SharedPreference({ false }, {}), connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", contactConnection = mutableStateOf(PendingContactConnection.getSampleData()), - share = {}, learnMore = {}, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 934c050d8..d04a85d90 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -131,13 +131,13 @@ private fun ContactConnectionInfoLayout( SectionView { if (!connReq.isNullOrEmpty() && contactConnection.initiated) { - QRCode( + SimpleXLinkQRCode( connReq, Modifier .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) .aspectRatio(1f) ) incognitoEnabled() - ShareLinkButton(share) + ShareLinkButton(connReq) OneTimeLinkLearnMoreButton(learnMore) } else { incognitoEnabled() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt index 0c13bd4f6..f7a5a1e86 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt @@ -53,32 +53,14 @@ fun PasteToConnectLayout( fun connectViaLink(connReqUri: String) { try { val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - val action = suspend { - Log.d(TAG, "connectViaUri: connecting") - if (connectViaUri(chatModel, linkType, uri, incognito = incognito.value)) { - close() - } - } - if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), - confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { action() } } - ) - } else action() + withApi { + planAndConnect(chatModel, uri, incognito = incognito.value, close) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.invalid_connection_link), text = generalGetString(MR.strings.this_string_is_not_a_connection_link) ) - } catch (e: URISyntaxException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_connection_link), - text = generalGetString(MR.strings.this_string_is_not_a_connection_link) - ) } } @@ -115,6 +97,7 @@ fun PasteToConnectLayout( painterResource(MR.images.ic_link), stringResource(MR.strings.connect_button), click = { connectViaLink(connectionLink.value) }, + disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ") ) IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index 663292596..763addae6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -19,6 +19,29 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.launch +@Composable +fun SimpleXLinkQRCode( + connReq: String, + modifier: Modifier = Modifier, + tintColor: Color = Color(0xff062d56), + withLogo: Boolean = true +) { + QRCode( + simplexChatLink(connReq), + modifier, + tintColor, + withLogo + ) +} + +fun simplexChatLink(uri: String): String { + return if (uri.startsWith("simplex:/")) { + uri.replace("simplex:/", "https://simplex.chat/") + } else { + uri + } +} + @Composable fun QRCode( connReq: String, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt index e3fa92275..2f52c2cac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.newchat import SectionBottomSpacer +import SectionItemView import SectionTextFooter import androidx.compose.desktop.ui.tooling.preview.Preview import chat.simplex.common.platform.Log @@ -10,66 +11,265 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.TAG import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.openDirectChat +import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.net.URI @Composable expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) enum class ConnectionLinkType { - CONTACT, INVITATION, GROUP + INVITATION, CONTACT, GROUP } -@Serializable -sealed class CReqClientData { - @Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData() -} - -fun withUriAction(uri: URI, run: suspend (ConnectionLinkType) -> Unit) { - val action = uri.path?.drop(1)?.replace("/", "") - val data = URI(uri.toString().replaceFirst("#/", "/")).getQueryParameter("data") - val type = when { - data != null -> { - val parsed = runCatching { - json.decodeFromString(CReqClientData.serializer(), data) +suspend fun planAndConnect( + chatModel: ChatModel, + uri: URI, + incognito: Boolean?, + close: (() -> Unit)? +) { + val connectionPlan = chatModel.controller.apiConnectPlan(uri.toString()) + if (connectionPlan != null) { + when (connectionPlan) { + is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { + InvitationLinkPlan.Ok -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") + if (incognito != null) { + connectViaUri(chatModel, uri, incognito, connectionPlan, close) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_via_invitation_link), + text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), + connectDestructive = false + ) + } + } + InvitationLinkPlan.OwnLink -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link), + confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)), + connectDestructive = true + ) + } + } + is InvitationLinkPlan.Connecting -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito") + val contact = connectionPlan.invitationLinkPlan.contact_ + if (contact != null) { + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_already_connecting), + generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + ) + } + } + is InvitationLinkPlan.Known -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") + val contact = connectionPlan.invitationLinkPlan.contact + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + ) + } } - when { - parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP - else -> null + is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) { + ContactAddressPlan.Ok -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") + if (incognito != null) { + connectViaUri(chatModel, uri, incognito, connectionPlan, close) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_via_contact_link), + text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), + connectDestructive = false + ) + } + } + ContactAddressPlan.OwnLink -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address), + confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)), + connectDestructive = true + ) + } + } + ContactAddressPlan.ConnectingConfirmReconnect -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_repeat_connection_request), + text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address), + confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_repeat_connection_request), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)), + connectDestructive = true + ) + } + } + is ContactAddressPlan.ConnectingProhibit -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") + val contact = connectionPlan.contactAddressPlan.contact + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + ) + } + is ContactAddressPlan.Known -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") + val contact = connectionPlan.contactAddressPlan.contact + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + ) + } + } + is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { + GroupLinkPlan.Ok -> { + Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_via_group_link), + text = generalGetString(MR.strings.you_will_join_group), + confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } } + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_via_group_link), + text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)), + connectDestructive = false + ) + } + } + is GroupLinkPlan.OwnLink -> { + Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito") + val groupInfo = connectionPlan.groupLinkPlan.groupInfo + ownGroupLinkConfirmConnect(chatModel, uri, incognito, connectionPlan, groupInfo, close) + } + GroupLinkPlan.ConnectingConfirmReconnect -> { + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_repeat_join_request), + text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), + confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_repeat_join_request), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)), + connectDestructive = true + ) + } + } + is GroupLinkPlan.ConnectingProhibit -> { + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") + val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ + if (groupInfo != null) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_already_joining_the_group), + generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + ) + } + } + is GroupLinkPlan.Known -> { + Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") + val groupInfo = connectionPlan.groupLinkPlan.groupInfo + openKnownGroup(chatModel, close, groupInfo) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + ) + } } } - - action == "contact" -> ConnectionLinkType.CONTACT - action == "invitation" -> ConnectionLinkType.INVITATION - else -> null - } - if (type != null) { - withApi { run(type) } } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_contact_link), - text = generalGetString(MR.strings.this_link_is_not_a_valid_connection_link) - ) + Log.d(TAG, "planAndConnect, plan error") + if (incognito != null) { + connectViaUri(chatModel, uri, incognito, connectionPlan = null, close) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan = null, close, + title = generalGetString(MR.strings.connect_plan_connect_via_link), + connectDestructive = false + ) + } } } -suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: URI, incognito: Boolean): Boolean { +suspend fun connectViaUri( + chatModel: ChatModel, + uri: URI, + incognito: Boolean, + connectionPlan: ConnectionPlan?, + close: (() -> Unit)? +): Boolean { val r = chatModel.controller.apiConnect(incognito, uri.toString()) + val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION if (r) { + close?.invoke() AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = - when (action) { + when (connLinkType) { ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted) ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online) ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online) @@ -79,6 +279,139 @@ suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: return r } +fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType { + return when(connectionPlan) { + is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION + is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT + is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP + } +} + +fun askCurrentOrIncognitoProfileAlert( + chatModel: ChatModel, + uri: URI, + connectionPlan: ConnectionPlan?, + close: (() -> Unit)?, + title: String, + text: AnnotatedString? = null, + connectDestructive: Boolean, +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = title, + text = text, + buttons = { + Column { + val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = false, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) + } + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = true, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +fun openKnownContact(chatModel: ChatModel, close: (() -> Unit)?, contact: Contact) { + withApi { + val c = chatModel.getContactChat(contact.contactId) + if (c != null) { + close?.invoke() + openDirectChat(contact.contactId, chatModel) + } + } +} + +fun ownGroupLinkConfirmConnect( + chatModel: ChatModel, + uri: URI, + incognito: Boolean?, + connectionPlan: ConnectionPlan?, + groupInfo: GroupInfo, + close: (() -> Unit)?, +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.connect_plan_join_your_group), + text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)), + buttons = { + Column { + // Open group + SectionItemView({ + AlertManager.shared.hideAlert() + openKnownGroup(chatModel, close, groupInfo) + }) { + Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (incognito != null) { + // Join incognito / Join with current profile + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito, connectionPlan, close) + } + }) { + Text( + if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), + Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error + ) + } + } else { + // Use current profile + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = false, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Use new incognito profile + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = true, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupInfo) { + withApi { + val g = chatModel.getGroupChat(groupInfo.groupId) + if (g != null) { + close?.invoke() + openGroupChat(groupInfo.groupId, chatModel) + } + } +} + @Composable fun ConnectContactLayout( chatModel: ChatModel, @@ -92,21 +425,8 @@ fun ConnectContactLayout( QRCodeScanner { connReqUri -> try { val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - val action = suspend { - Log.d(TAG, "connectViaUri: connecting") - if (connectViaUri(ChatModel, linkType, uri, incognito = incognito.value)) { - close() - } - } - if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), - confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { action() } } - ) - } else action() + withApi { + planAndConnect(chatModel, uri, incognito = incognito.value, close) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 72cbc3a62..013222338 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -18,7 +18,8 @@ import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.views.newchat.SimpleXLinkQRCode +import chat.simplex.common.views.newchat.simplexChatLink import chat.simplex.res.MR @Composable @@ -38,7 +39,7 @@ fun CreateSimpleXAddress(m: ChatModel) { sendEmail = { address -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(address.connReqContact) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink(address.connReqContact)) ) }, createAddress = { @@ -91,8 +92,8 @@ private fun CreateSimpleXAddressLayout( Spacer(Modifier.weight(1f)) if (userAddress != null) { - QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { share(userAddress.connReqContact) } + SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } Spacer(Modifier.weight(1f)) ShareViaEmailButton { sendEmail(userAddress) } Spacer(Modifier.weight(1f)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 63f06a2ae..d03b75856 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -24,10 +24,10 @@ import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.ShareAddressButton import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.model.ChatModel import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.* +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @Composable @@ -100,7 +100,7 @@ fun UserAddressView( sendEmail = { userAddress -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(userAddress.connReqContact) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact)) ) }, setProfileAddress = ::setProfileAddress, @@ -201,8 +201,8 @@ private fun UserAddressLayout( val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { share(userAddress.connReqContact) } + SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } ShareViaEmailButton { sendEmail(userAddress) } ShareWithContactsButton(shareViaProfile, setProfileAddress) AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } 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 114fe49e9..bd59d236d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -4,13 +4,13 @@ k - Connect via contact link? - Connect via invitation link? - Connect via group link? + Connect via contact address? + Connect via one-time link? + Join group? Use current profile Use new incognito profile Your profile will be sent to the contact that you received this link from. - You will join a group this link refers to and connect to its group members. + You will connect to all group members. Connect Connect incognito @@ -1600,4 +1600,25 @@ Coming soon! This feature is not yet supported. Try the next release. + + + Connect to yourself? + This is your own one-time link! + You are already connecting to %1$s. + Already connecting! + You are already connecting via this one-time link! + This is your own SimpleX address! + Repeat connection request? + You have already requested connection via this address! + Join your group? + This is your link for group %1$s! + Open group + Repeat join request? + You are already joining the group via this link! + Group already exists! + You are already joining the group %1$s. + Already joining the group! + You are already joining the group via this link. + You are already in group %1$s. + Connect via link? \ No newline at end of file