android, desktop, ios: connect via SimpleX name

This commit is contained in:
shum
2026-06-11 10:27:31 +00:00
parent 0706b2516e
commit f7c89890ad
13 changed files with 106 additions and 59 deletions
+15
View File
@@ -1083,6 +1083,21 @@ private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> 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)",
@@ -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
@@ -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
@@ -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
+10 -25
View File
@@ -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)
+3
View File
@@ -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
@@ -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()
@@ -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) {
@@ -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()
@@ -31,11 +31,6 @@ suspend fun planAndConnect(
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
): CompletableDeferred<Boolean> {
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)
@@ -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()
@@ -679,7 +679,11 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, 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<Boolean>,
@@ -199,6 +199,12 @@
<string name="channel_name_requires_newer_app_version">Connecting via channel name requires a newer app version.</string>
<string name="contact_name_requires_newer_app_version">Connecting via contact name requires a newer app version.</string>
<string name="please_upgrade_the_app">Please upgrade the app.</string>
<string name="simplex_name_not_found">SimpleX name not found</string>
<string name="simplex_name_not_found_desc">There is no contact or group registered with this SimpleX name.</string>
<string name="cannot_reconnect_via_simplex_name">Cannot reconnect via name</string>
<string name="simplex_name_unprepared_desc">This SimpleX name is known but has no saved link to reconnect via.</string>
<string name="simplex_name_resolution_unavailable">Name resolution unavailable</string>
<string name="simplex_name_resolver_unavailable_desc">None of your SMP servers support resolving SimpleX names. Add a server that does, or use a connection link.</string>
<string name="channel_temporarily_unavailable">Channel temporarily unavailable</string>
<string name="channel_no_active_relays_try_later">Channel has no active relays. Please try to join later.</string>
<string name="app_update_required">App update required</string>