diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ea2de31569..6b7bc35c48 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1083,6 +1083,21 @@ private func apiConnectResponseAlert(_ r: APIResult) -> Alert { title: "Unsupported connection link", message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." ) + case let .error(.simplexNameNotFound(name)): + mkAlert( + title: name.nameType == .contact ? "Contact name not found" : "Channel name not found", + message: "There is no contact or group registered with this SimpleX name." + ) + case .error(.simplexNameUnprepared): + mkAlert( + title: "Cannot reconnect via name", + message: "This SimpleX name is known but has no saved link to reconnect via." + ) + case .error(.simplexNameResolverUnavailable): + mkAlert( + title: "Name resolution unavailable", + message: "None of your SMP servers support resolving SimpleX names. Add a server that does, or use a connection link." + ) case .errorAgent(.SMP(_, .AUTH)): mkAlert( title: "Connection error (AUTH)", diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 11c3c4c3f4..a860167359 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -214,8 +214,10 @@ private func handleTextTaps( var simplex: Bool = false s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in if index >= range.location && index < range.location + range.length { - if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo { - showUnsupportedNameAlert(nameInfo) + if attrs[nameAttrKey] is SimplexNameInfo { + // Route the tapped name through the same connect flow as a link; + // planAndConnect resolves it on the core (name target). + planAndConnect(s.attributedSubstring(from: range).string, theme: theme, dismiss: false) } else if let url = attrs[linkAttrKey] as? String { linkURL = url browser = attrs[webLinkAttrKey] != nil diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index d90149c7dd..6e745d1bd3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -683,8 +683,9 @@ struct ChatListSearchBar: View { searchShowingSimplexLink = true searchChatFilteredBySimplexLink = nil connect(text) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + searchFocussed = false + connect(text) case .none: if t != "" { searchFocussed = true diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index f99b03086e..177225921c 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -389,8 +389,9 @@ struct ContactsListSearchBar: View { searchShowingSimplexLink = true searchChatFilteredBySimplexLink = nil connect(text) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + searchFocussed = false + connect(text) case .none: if t != "" { searchFocussed = true diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 67fd353ebc..640b31a13a 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -669,8 +669,9 @@ private struct ConnectView: View { case let .link(text, _, _): pastedLink = text connect(pastedLink) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + pastedLink = text + connect(pastedLink) case .none: alert = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), @@ -869,7 +870,7 @@ func strIsSimplexLink(_ str: String) -> Bool { enum ConnectTarget { case link(text: String, linkType: SimplexLinkType, linkText: String) - case name(SimplexNameInfo) + case name(text: String, nameInfo: SimplexNameInfo) } func strConnectTarget(_ str: String) -> ConnectTarget? { @@ -878,28 +879,14 @@ func strConnectTarget(_ str: String) -> ConnectTarget? { return if links.count == 1, case let .simplexLink(_, linkType, _, smpHosts) = links[0].format { .link(text: links[0].text, linkType: linkType, linkText: simplexLinkText(linkType, smpHosts)) } else if links.isEmpty, - case let .simplexName(nameInfo) = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } })?.format { - .name(nameInfo) + let nameFt = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } }), + case let .simplexName(nameInfo) = nameFt.format { + .name(text: nameFt.text, nameInfo: nameInfo) } else { nil } } -func showUnsupportedNameAlert(_ nameInfo: SimplexNameInfo) { - let upgrade = " " + NSLocalizedString("Please upgrade the app.", comment: "alert message") - if nameInfo.nameType == .contact { - showAlert( - NSLocalizedString("Unsupported contact name", comment: "alert title"), - message: NSLocalizedString("Connecting via contact name requires a newer app version.", comment: "alert message") + upgrade - ) - } else { - showAlert( - NSLocalizedString("Unsupported channel name", comment: "alert title"), - message: NSLocalizedString("Connecting via channel name requires a newer app version.", comment: "alert message") + upgrade - ) - } -} - struct IncognitoToggle: View { @EnvironmentObject var theme: AppTheme @Binding var incognitoEnabled: Bool @@ -1319,10 +1306,6 @@ func planAndConnect( filterKnownGroup: ((GroupInfo) -> Void)? = nil ) { switch strConnectTarget(shortOrFullLink) { - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) - cleanup?() - return case let .link(_, linkType, _): if linkType == .relay { showAlert( @@ -1332,7 +1315,9 @@ func planAndConnect( cleanup?() return } - case .none: break + // A SimplexName falls through to apiConnectPlan, which resolves it on the + // core (the /_connect plan command accepts a name target, not only a link). + case .name, .none: break } ConnectProgressManager.shared.cancelConnectProgress() let inProgress = BoxedValue(true) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5f1d8ef6c2..406539cd34 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -741,6 +741,9 @@ public enum ChatErrorType: Decodable, Hashable { case chatNotStopped case chatStoreChanged case invalidConnReq + case simplexNameNotFound(simplexName: SimplexNameInfo) + case simplexNameUnprepared(simplexName: SimplexNameInfo) + case simplexNameResolverUnavailable(simplexName: SimplexNameInfo) case unsupportedConnReq case invalidChatMessage(connection: Connection, message: String) case connReqMessageProhibited 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 54986e1815..e0bb8090d7 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 @@ -1555,6 +1555,27 @@ object ChatController { generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade) ) } + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.SimplexNameNotFound -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_not_found), + generalGetString(MR.strings.simplex_name_not_found_desc) + ) + } + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.SimplexNameUnprepared -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cannot_reconnect_via_simplex_name), + generalGetString(MR.strings.simplex_name_unprepared_desc) + ) + } + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.SimplexNameResolverUnavailable -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_resolution_unavailable), + generalGetString(MR.strings.simplex_name_resolver_unavailable_desc) + ) + } r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.SMP && r.err.agentError.smpErr is SMPErrorType.AUTH -> { @@ -1592,6 +1613,12 @@ object ChatController { generalGetString(MR.strings.invalid_connection_link) e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq -> generalGetString(MR.strings.unsupported_connection_link) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.SimplexNameNotFound -> + generalGetString(MR.strings.simplex_name_not_found) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.SimplexNameUnprepared -> + generalGetString(MR.strings.cannot_reconnect_via_simplex_name) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.SimplexNameResolverUnavailable -> + generalGetString(MR.strings.simplex_name_resolution_unavailable) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH -> generalGetString(MR.strings.connection_error_auth) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED -> @@ -7297,6 +7324,9 @@ sealed class ChatErrorType { is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is SimplexNameNotFound -> "simplexNameNotFound" + is SimplexNameUnprepared -> "simplexNameUnprepared" + is SimplexNameResolverUnavailable -> "simplexNameResolverUnavailable" is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ConnReqMessageProhibited -> "connReqMessageProhibited" @@ -7379,6 +7409,9 @@ sealed class ChatErrorType { @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() + @Serializable @SerialName("simplexNameNotFound") class SimplexNameNotFound(val simplexName: SimplexNameInfo): ChatErrorType() + @Serializable @SerialName("simplexNameUnprepared") class SimplexNameUnprepared(val simplexName: SimplexNameInfo): ChatErrorType() + @Serializable @SerialName("simplexNameResolverUnavailable") class SimplexNameResolverUnavailable(val simplexName: SimplexNameInfo): ChatErrorType() @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index c9f7d96f39..c7c96cd731 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -338,13 +338,10 @@ fun MarkdownText ( withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } withAnnotation("SIMPLEX_NAME") { a -> val idx = a.item.toIntOrNull() - val nameInfo = (idx?.let { formattedText.getOrNull(it) }?.format as? Format.SimplexName)?.nameInfo - val (title, msg) = if (nameInfo?.nameType == SimplexNameType.contact) { - generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version) - } else { - generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version) - } - AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}") + val nameText = idx?.let { formattedText.getOrNull(it) }?.text + // The name string is routed through the same connect path as a + // link; planAndConnect resolves it on the core (name target). + if (nameText != null) uriHandler.openVerifiedSimplexUri(nameText) } } if (hasSecrets) { 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 3012525f9b..05255a4bb1 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 @@ -800,7 +800,10 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState searchChatFilteredBySimplexLink.value = null connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + hideKeyboard(view) + connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { if (it.isNotEmpty()) { focusRequester.requestFocus() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index e5dbe01d68..3840cf466e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -31,11 +31,6 @@ suspend fun planAndConnect( filterKnownGroup: ((GroupInfo) -> Unit)? = null, ): CompletableDeferred { when (val target = strConnectTarget(shortOrFullLink.trim())) { - is ConnectTarget.Name -> { - showUnsupportedNameAlert(target.nameInfo) - cleanup?.invoke() - return CompletableDeferred(false) - } is ConnectTarget.Link -> { if (target.linkType == SimplexLinkType.relay) { AlertManager.privacySensitive.showAlertMsg( @@ -46,7 +41,9 @@ suspend fun planAndConnect( return CompletableDeferred(false) } } - null -> {} + // A SimplexName falls through to apiConnectPlan, which resolves it on the + // core (the /_connect plan command accepts a name target, not only a link). + is ConnectTarget.Name, null -> {} } connectProgressManager.cancelConnectProgress() val inProgress = mutableStateOf(true) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index af7f59496b..41bf920a48 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -536,7 +536,15 @@ private fun ContactsSearchBar( cleanup = { searchText.value = TextFieldValue() } ) } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + hideKeyboard(view) + connect( + link = target.text, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + cleanup = { searchText.value = TextFieldValue() } + ) + } null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { if (it.isNotEmpty()) { focusRequester.requestFocus() 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 6799fa1300..d3bca178aa 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 @@ -679,7 +679,11 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC showQRCodeScanner.value = false withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } } } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + pastedLink.value = target.text + showQRCodeScanner.value = false + withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } } + } null -> AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.invalid_contact_link), text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) @@ -824,7 +828,7 @@ fun strIsSimplexLink(str: String): Boolean { sealed class ConnectTarget { class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget() - class Name(val nameInfo: SimplexNameInfo) : ConnectTarget() + class Name(val text: String, val nameInfo: SimplexNameInfo) : ConnectTarget() } fun strConnectTarget(str: String): ConnectTarget? { @@ -835,21 +839,13 @@ fun strConnectTarget(str: String): ConnectTarget? { return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText) } if (links.isEmpty()) { - val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo } - if (nameInfo != null) return ConnectTarget.Name(nameInfo) + val nameFt = parsedMd.firstOrNull { it.format is Format.SimplexName } + val nameInfo = (nameFt?.format as? Format.SimplexName)?.nameInfo + if (nameFt != null && nameInfo != null) return ConnectTarget.Name(nameFt.text, nameInfo) } return null } -fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) { - val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) { - generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version) - } else { - generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version) - } - AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}") -} - @Composable fun IncognitoToggle( incognitoPref: SharedPreference, 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 71c0bf98a4..2917dd4a35 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -199,6 +199,12 @@ Connecting via channel name requires a newer app version. Connecting via contact name requires a newer app version. Please upgrade the app. + SimpleX name not found + There is no contact or group registered with this SimpleX name. + Cannot reconnect via name + This SimpleX name is known but has no saved link to reconnect via. + Name resolution unavailable + None of your SMP servers support resolving SimpleX names. Add a server that does, or use a connection link. Channel temporarily unavailable Channel has no active relays. Please try to join later. App update required