android, desktop, ios: open known contact on name lookup; surface prepared contact

Name search opens the contact (not list-filter); resolved/prepared contacts and groups are added to the chat list so they're visible and openable. Kotlin compile-verified; iOS edits pattern-matched, pending Xcode build.
This commit is contained in:
shum
2026-06-11 12:43:30 +00:00
parent f7c89890ad
commit b267bca6ab
7 changed files with 69 additions and 11 deletions
@@ -216,8 +216,9 @@ private func handleTextTaps(
if index >= range.location && index < range.location + range.length {
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)
// planAndConnect resolves it on the core (name target). This runs
// in a free function with no view context, so use the global theme.
planAndConnect(s.attributedSubstring(from: range).string, theme: AppTheme.shared, dismiss: false)
} else if let url = attrs[linkAttrKey] as? String {
linkURL = url
browser = attrs[webLinkAttrKey] != nil
@@ -684,8 +684,18 @@ struct ChatListSearchBar: View {
searchChatFilteredBySimplexLink = nil
connect(text)
case let .name(text, _):
// A name lookup means "take me to this contact": open it (visible prompt),
// unlike a pasted link in search which filters the list so no filterKnownContact.
searchFocussed = false
connect(text)
planAndConnect(
text,
theme: theme,
dismiss: false,
cleanup: {
searchText = ""
searchFocussed = false
}
)
case .none:
if t != "" {
searchFocussed = true
@@ -390,8 +390,18 @@ struct ContactsListSearchBar: View {
searchChatFilteredBySimplexLink = nil
connect(text)
case let .name(text, _):
// A name lookup means "take me to this contact": open it (visible prompt),
// unlike a pasted link in search which filters the list so no filterKnownContact.
searchFocussed = false
connect(text)
planAndConnect(
text,
theme: theme,
dismiss: true,
cleanup: {
searchText = ""
searchFocussed = false
}
)
case .none:
if t != "" {
searchFocussed = true
@@ -1463,6 +1463,12 @@ func planAndConnect(
case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known")
await MainActor.run {
// A name-resolved contact is prepared in the store but not yet in the
// chat list (link-prepared chats arrive via NewPreparedChat). Surface it
// so it's visible and openable; no-op if already present.
if ChatModel.shared.getContactChat(contact.contactId) == nil {
ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact)))
}
if let f = filterKnownContact {
f(contact)
} else {
@@ -1542,6 +1548,11 @@ func planAndConnect(
case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known")
await MainActor.run {
// Same as .contactAddress .known: surface a name-resolved (prepared)
// group in the chat list so it's visible and openable.
if ChatModel.shared.getGroupChat(groupInfo.groupId) == nil {
ChatModel.shared.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil)))
}
if let f = filterKnownGroup {
f(groupInfo)
} else {
@@ -801,8 +801,18 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
}
is ConnectTarget.Name -> {
// A name lookup means "take me to this contact": open the chat if
// it's already known (visible prompt), unlike a pasted link which
// filters the list. So no filterKnownContact here.
hideKeyboard(view)
connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
withBGApi {
planAndConnect(
chatModel.remoteHostId(),
target.text,
close = null,
cleanup = { searchText.value = TextFieldValue() },
)
}
}
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
@@ -201,6 +201,12 @@ private suspend fun planAndConnectTask(
is ContactAddressPlan.Known -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Known")
val contact = connectionPlan.contactAddressPlan.contact
// A name-resolved contact is prepared in the store but not yet in the
// chat list (link-prepared chats arrive via NewPreparedChat). Surface it
// so it's visible and openable; no-op if already present.
if (chatModel.getContactChat(contact.contactId) == null) {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList()))
}
if (filterKnownContact != null) {
filterKnownContact(contact)
} else {
@@ -285,6 +291,11 @@ private suspend fun planAndConnectTask(
is GroupLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .GroupLink, .Known")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
// Same as ContactAddress.Known: surface a name-resolved (prepared)
// group in the chat list so it's visible and openable.
if (chatModel.getGroupChat(groupInfo.groupId) == null) {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo, groupChatScope = null), chatItems = emptyList()))
}
if (filterKnownGroup != null) {
filterKnownGroup(groupInfo)
} else {
@@ -537,13 +537,18 @@ private fun ContactsSearchBar(
)
}
is ConnectTarget.Name -> {
// A name lookup means "take me to this contact": open the chat if
// it's already known (visible prompt), unlike a pasted link which
// filters the list. So no filterKnownContact here.
hideKeyboard(view)
connect(
link = target.text,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
cleanup = { searchText.value = TextFieldValue() }
)
withBGApi {
planAndConnect(
chatModel.remoteHostId(),
target.text,
close = close,
cleanup = { searchText.value = TextFieldValue() },
)
}
}
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {