diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index a860167359..6b15053cac 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -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 diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 6e745d1bd3..b70904e34e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -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 diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 177225921c..32848bb219 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -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 diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 640b31a13a..7e123a584f 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -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 { 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 05255a4bb1..99dec506e7 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 @@ -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()) { 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 3840cf466e..9ed6dd0f19 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 @@ -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 { 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 41bf920a48..5da29e5851 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 @@ -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()) {