mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-30 22:40:24 +00:00
ui: show "connect to name" button starting with 5 characters; ios: show button in New chat view search (#7318)
* ui: show "connect to name" button starting with 5 characters * show button in new chat view search
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -1039,8 +1039,8 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user