diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 27840a07c2..b05e0696e3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -649,7 +649,13 @@ struct ChatListSearchBar: View { // a typed name shows a row to connect to it (as on Android mobile): with the reachable toolbar it // replaces the tags above the search field; in top bar mode the tags stay and it moves below (end of VStack) if oneHandUI, let candidate = connectNameCandidate { - connectByNameRow(candidate) + ConnectByNameRow( + name: candidate, + searchText: $searchText, + connectNameCandidate: $connectNameCandidate, + searchFocussed: $searchFocussed, + dismiss: false + ) } else { ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } } @@ -688,7 +694,13 @@ struct ChatListSearchBar: View { } } if !oneHandUI, let candidate = connectNameCandidate { - connectByNameRow(candidate) + ConnectByNameRow( + name: candidate, + searchText: $searchText, + connectNameCandidate: $connectNameCandidate, + searchFocussed: $searchFocussed, + dismiss: false + ) } } .onChange(of: searchFocussed) { sf in @@ -770,33 +782,6 @@ struct ChatListSearchBar: View { } } - // Row shown in place of the list tags when the search text is a SimpleX name. The @ icon marks a - // contact name, the tag icon a channel/other name; tapping hides the keyboard, connects online, and - // clears the field. - private func connectByNameRow(_ name: String) -> some View { - HStack(spacing: 4) { - Image(systemName: name.hasPrefix("@") ? "at" : "number") - .foregroundColor(theme.colors.primary) - Text(String.localizedStringWithFormat(NSLocalizedString("Connect to %@", comment: "new chat action"), name)) - .foregroundColor(theme.colors.primary) - Spacer() - } - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) - .onTapGesture { - searchFocussed = false - planAndConnect( - name, - theme: theme, - dismiss: false, - cleanup: { - searchText = "" - connectNameCandidate = nil - } - ) - } - } - private func connect(_ link: String) { planAndConnect( link, @@ -812,11 +797,47 @@ struct ChatListSearchBar: View { } } +// Row shown when the search text is a SimpleX name — in place of the list tags in the chat list, below +// the search field in the new chat sheet. The @ icon marks a contact name, the tag icon a channel/other +// name; tapping hides the keyboard, connects online, and clears the field. +struct ConnectByNameRow: View { + @EnvironmentObject var theme: AppTheme + var name: String + @Binding var searchText: String + @Binding var connectNameCandidate: String? + @FocusState.Binding var searchFocussed: Bool + var dismiss: Bool + + var body: some View { + HStack(spacing: 4) { + Image(systemName: name.hasPrefix("@") ? "at" : "number") + .foregroundColor(theme.colors.primary) + Text(String.localizedStringWithFormat(NSLocalizedString("Connect to %@", comment: "new chat action"), name)) + .foregroundColor(theme.colors.primary) + Spacer() + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture { + searchFocussed = false + planAndConnect( + name, + theme: theme, + dismiss: dismiss, + cleanup: { + searchText = "" + connectNameCandidate = nil + } + ) + } + } +} + // Default top-level part used to complete a bare name typed in the search field (search field only; // the message parser and the wire format are unchanged). private let DEFAULT_NAME_TLD = "testing" -// Shortest name that offers the button, so it is discoverable but does not flash on a single letter. -private let MIN_NAME_LENGTH = 2 +// Shortest name that offers the button, so it is discoverable but does not flash on short prefixes. +private let MIN_NAME_LENGTH = 5 private func isNameLabel(_ s: String) -> Bool { s.count >= 1 && s.count <= 63 && s.range(of: "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$", options: .regularExpression) != nil diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 4690bf82a8..416dc32308 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -41,6 +41,8 @@ struct NewChatSheet: View { @State private var searchText = "" @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil + // when the search text is a SimpleX name, the string to connect to (with @/# preserved); nil otherwise + @State private var connectNameCandidate: String? = nil @State private var alert: SomeAlert? // Sheet height management @@ -81,15 +83,25 @@ struct NewChatSheet: View { private func viewBody(_ showArchive: Bool) -> some View { List { - HStack { + VStack(spacing: 12) { ContactsListSearchBar( searchMode: $searchMode, searchFocussed: $searchFocussed, searchText: $searchText, searchShowingSimplexLink: $searchShowingSimplexLink, - searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + connectNameCandidate: $connectNameCandidate ) .frame(maxWidth: .infinity) + if let candidate = connectNameCandidate { + ConnectByNameRow( + name: candidate, + searchText: $searchText, + connectNameCandidate: $connectNameCandidate, + searchFocussed: $searchFocussed, + dismiss: true + ) + } } .listRowSeparator(.hidden) .listRowBackground(Color.clear) @@ -327,6 +339,7 @@ struct ContactsListSearchBar: View { @Binding var searchText: String @Binding var searchShowingSimplexLink: Bool @Binding var searchChatFilteredBySimplexLink: String? + @Binding var connectNameCandidate: String? @State private var ignoreSearchTextChange = false @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @@ -381,33 +394,32 @@ struct ContactsListSearchBar: View { if ignoreSearchTextChange { ignoreSearchTextChange = false } else { - switch strConnectTarget(t.trimmingCharacters(in: .whitespaces)) { + let s = t.trimmingCharacters(in: .whitespaces) + switch strConnectTarget(s) { case let .link(text, _, linkText): searchFocussed = false ignoreSearchTextChange = true searchText = linkText searchShowingSimplexLink = true searchChatFilteredBySimplexLink = nil + connectNameCandidate = nil connect(text) - case let .name(text, _): - searchFocussed = false - planAndConnect( - text, - theme: theme, - dismiss: true, - cleanup: { - searchText = "" - searchFocussed = false + default: + // A name is resolved only when its "Connect to …" row is tapped, not on every keystroke. + // The simplex-name filter is chat-list only: this contacts/deleted view is a scoped + // subset, so a resolved chat id (channel, business, unlisted or active-only contact) + // may not be present in it. + let candidate = nameSearchCandidate(s) + connectNameCandidate = candidate + if candidate == nil { + if t != "" { + searchFocussed = true + } else { + connectProgressManager.cancelConnectProgress() } - ) - case .none: - if t != "" { - searchFocussed = true - } else { - connectProgressManager.cancelConnectProgress() + searchShowingSimplexLink = false + searchChatFilteredBySimplexLink = nil } - searchShowingSimplexLink = false - searchChatFilteredBySimplexLink = nil } } } @@ -449,7 +461,9 @@ struct DeletedChats: View { @State private var searchText = "" @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil - + // deleted contacts are not connected to by name, so this candidate only stops per-keystroke resolution + @State private var connectNameCandidate: String? = nil + var body: some View { List { ContactsListSearchBar( @@ -457,7 +471,8 @@ struct DeletedChats: View { searchFocussed: $searchFocussed, searchText: $searchText, searchShowingSimplexLink: $searchShowingSimplexLink, - searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + connectNameCandidate: $connectNameCandidate ) .listRowSeparator(.hidden) .listRowBackground(Color.clear) 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 439d1af189..77b4c40d7d 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 @@ -1039,8 +1039,8 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat // Default top-level part used to complete a bare name typed in the search field (search field only; // the message parser and the wire format are unchanged). private const val DEFAULT_NAME_TLD = "testing" -// Shortest name that offers the button, so it is discoverable but does not flash on a single letter. -private const val MIN_NAME_LENGTH = 2 +// Shortest name that offers the button, so it is discoverable but does not flash on short prefixes. +private const val MIN_NAME_LENGTH = 5 // Wait this long after the last keystroke before the local name search runs. internal const val NAME_SEARCH_DEBOUNCE_MS = 300L