diff options
8 files changed, 63 insertions, 34 deletions
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationDataSourceProtocol.swift b/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationDataSourceProtocol.swift index 057a82114c..52f11fad54 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationDataSourceProtocol.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationDataSourceProtocol.swift @@ -70,8 +70,11 @@ extension LocationDataSourceProtocol { func search(by text: String) { nodes.forEachNode { node in node.isHiddenFromSearch = false + node.searchWeight = 0 node.showsChildren = false } + let text = text.trimmingCharacters(in: .whitespaces) + guard !text.isEmpty else { return } @@ -84,10 +87,21 @@ extension LocationDataSourceProtocol { } private func hideInSearch(node: LocationNode, searchText: String) -> Bool { - let matchesSelf = node.name.fuzzyMatch(searchText) + var searchWeight = 0 + if node.name.lowercased().hasPrefix(searchText.lowercased()) { + searchWeight = 3 + } else if node.name.lowercased().contains(" \(searchText.lowercased())") { + searchWeight = 2 + } else if node.name.lowercased().contains(searchText.lowercased()) { + searchWeight = 1 + } + print(node.name, node.searchWeight) + let matchesSelf = searchWeight > 0 var childMatches = false + var maxChildWeight = searchWeight for child in node.children where !hideInSearch(node: child, searchText: searchText) { childMatches = true + maxChildWeight = max(maxChildWeight, child.searchWeight) } if matchesSelf && !childMatches { node.forEachDescendant { child in @@ -96,6 +110,7 @@ extension LocationDataSourceProtocol { } } node.isHiddenFromSearch = !matchesSelf && !childMatches + node.searchWeight = maxChildWeight node.showsChildren = childMatches return node.isHiddenFromSearch } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationNode.swift b/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationNode.swift index 59db93fa9c..66c308a5fe 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationNode.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationNode.swift @@ -17,6 +17,7 @@ class LocationNode: @unchecked Sendable { weak var parent: LocationNode? var children: [LocationNode] var showsChildren: Bool + var searchWeight: Int var isHiddenFromSearch: Bool var isConnected: Bool var isSelected: Bool @@ -30,6 +31,7 @@ class LocationNode: @unchecked Sendable { parent: LocationNode? = nil, children: [LocationNode] = [], showsChildren: Bool = false, + searchWeight: Int = 0, isHiddenFromSearch: Bool = false, isConnected: Bool = false, isSelected: Bool = false, @@ -42,6 +44,7 @@ class LocationNode: @unchecked Sendable { self.parent = parent self.children = children self.showsChildren = showsChildren + self.searchWeight = searchWeight self.isHiddenFromSearch = isHiddenFromSearch self.isConnected = isConnected self.isSelected = isSelected diff --git a/ios/MullvadVPN/View controllers/SelectLocation/Views/EntryLocationView.swift b/ios/MullvadVPN/View controllers/SelectLocation/Views/EntryLocationView.swift index 50f2ad203e..1b94cd00f1 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/Views/EntryLocationView.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/Views/EntryLocationView.swift @@ -2,6 +2,7 @@ import SwiftUI struct EntryLocationView<ViewModel: SelectLocationViewModel>: View { @ObservedObject var viewModel: ViewModel + let isSearching: Bool let onScrollOffsetChange: (CGFloat, CGFloat) -> Void var body: some View { if viewModel.showDAITAInfo { @@ -11,7 +12,7 @@ struct EntryLocationView<ViewModel: SelectLocationViewModel>: View { } else { ExitLocationView( viewModel: viewModel, context: $viewModel.entryContext, - onScrollOffsetChange: onScrollOffsetChange) + isSearching: isSearching, onScrollOffsetChange: onScrollOffsetChange) } } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/Views/ExitLocationView.swift b/ios/MullvadVPN/View controllers/SelectLocation/Views/ExitLocationView.swift index f492e04c97..ff39dc0feb 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/Views/ExitLocationView.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/Views/ExitLocationView.swift @@ -5,15 +5,11 @@ struct ExitLocationView<ViewModel: SelectLocationViewModel>: View { @Binding var context: LocationContext @State var newCustomListAlert: MullvadInputAlert? @State var alert: MullvadAlert? + let isSearching: Bool let onScrollOffsetChange: (CGFloat, CGFloat) -> Void @State private var previousScrollOffset: CGFloat = 0 var isShowingCustomListsSection: Bool { - viewModel.searchText.isEmpty - || (!viewModel.searchText.isEmpty - && !context.customLists - .filter { - !$0.isHiddenFromSearch - }.isEmpty) + viewModel.searchText.isEmpty && !isSearching } var isShowingAllLocationsSection: Bool { !context.locations.filter({ !$0.isHiddenFromSearch }).isEmpty @@ -53,6 +49,7 @@ struct ExitLocationView<ViewModel: SelectLocationViewModel>: View { } .zIndex(3) // prevent wrong overlapping during animations } + .id("container") .capturePosition(in: .exitLocationScroll) { frame in onScrollOffsetChange(previousScrollOffset, frame.minY) previousScrollOffset = frame.minY @@ -69,6 +66,13 @@ struct ExitLocationView<ViewModel: SelectLocationViewModel>: View { scrollProxy.scrollTo(selectedLocation.code, anchor: .center) } } + .onChange(of: isSearching) { + if isSearching { + withAnimation { + scrollProxy.scrollTo("container", anchor: .top) + } + } + } .accessibilityIdentifier(.selectLocationView) } .mullvadInputAlert(item: $newCustomListAlert) @@ -149,7 +153,7 @@ struct ExitLocationView<ViewModel: SelectLocationViewModel>: View { viewModel: viewModel, context: $viewModel.exitContext, newCustomListAlert: nil, - alert: nil, + alert: nil, isSearching: false, onScrollOffsetChange: { _, _ in } ) .background(Color.mullvadBackground) @@ -161,7 +165,7 @@ struct ExitLocationView<ViewModel: SelectLocationViewModel>: View { viewModel: viewModel, context: $viewModel.entryContext, newCustomListAlert: nil, - alert: nil, + alert: nil, isSearching: false, onScrollOffsetChange: { _, _ in } ) .background(Color.mullvadBackground) diff --git a/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationListItem.swift b/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationListItem.swift index 5991775b32..ae1165c74c 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationListItem.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationListItem.swift @@ -61,7 +61,7 @@ struct LocationListItem<ContextMenu>: View where ContextMenu: View { Image.mullvadIconTick .foregroundStyle(Color.mullvadSuccessColor) } - Text(location.name) + Text("\(location.name) (\(location.searchWeight))") .foregroundStyle( location.isActive && !location.isExcluded ? location.isSelected diff --git a/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationsListView.swift b/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationsListView.swift index 2a67fb6d0d..4f97e31260 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationsListView.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/Views/LocationsListView.swift @@ -9,6 +9,7 @@ struct LocationsListView<ContextMenu>: View where ContextMenu: View { var filteredLocationIndices: [Int] { locations .enumerated() + .sorted { $0.element.searchWeight > $1.element.searchWeight } .filter { !$0.element.isHiddenFromSearch } .map { $0.offset } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/Views/RelayItemView.swift b/ios/MullvadVPN/View controllers/SelectLocation/Views/RelayItemView.swift index 2fe9f0b22d..585bca3834 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/Views/RelayItemView.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/Views/RelayItemView.swift @@ -23,19 +23,19 @@ struct RelayItemView: View { switch multihopContext { case .entry: return """ - \(location.name) (\(String(localized: + \("\(location.name) (\(location.searchWeight))") (\(String(localized: String .LocalizationValue(MultihopContext.exit.description)))) """ case .exit: return """ - \(location.name) (\(String(localized: + \("\(location.name) (\(location.searchWeight))") (\(String(localized: String .LocalizationValue(MultihopContext.entry.description)))) """ } } - return "\(location.name)" + return "\(location.name) (\(location.searchWeight))" } var body: some View { diff --git a/ios/MullvadVPN/View controllers/SelectLocation/Views/SelectLocationView.swift b/ios/MullvadVPN/View controllers/SelectLocation/Views/SelectLocationView.swift index 1df2f5ace3..4c52857c1e 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/Views/SelectLocationView.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/Views/SelectLocationView.swift @@ -29,31 +29,34 @@ struct SelectLocationView<ViewModel>: View where ViewModel: SelectLocationViewMo // view, the top margin of the content is changed which solves the animation issues. ZStack(alignment: .topLeading) { VStack(spacing: 16) { - MultihopSelectionView( - hops: (viewModel.isMultihopEnabled ? MultihopContext.allCases : [MultihopContext.exit]) - .map { - let selectedLocation = - switch $0 { - case .entry: - viewModel.showDAITAInfo - ? LocationNode(name: "Automatic", code: "") - : viewModel.entryContext.selectedLocation - case .exit: viewModel.exitContext.selectedLocation - } - return Hop( - multihopContext: $0, - selectedLocation: selectedLocation - ) - }, - selectedMultihopContext: $viewModel.multihopContext, - isExpanded: headerIsExpanded - ) - .padding(.horizontal, 16) + if !focusSearchField && viewModel.searchText.isEmpty { + MultihopSelectionView( + hops: (viewModel.isMultihopEnabled ? MultihopContext.allCases : [MultihopContext.exit]) + .map { + let selectedLocation = + switch $0 { + case .entry: + viewModel.showDAITAInfo + ? LocationNode(name: "Automatic", code: "") + : viewModel.entryContext.selectedLocation + case .exit: viewModel.exitContext.selectedLocation + } + return Hop( + multihopContext: $0, + selectedLocation: selectedLocation + ) + }, + selectedMultihopContext: $viewModel.multihopContext, + isExpanded: headerIsExpanded + ) + .padding(.horizontal, 16) + } if showSearchField { MullvadSecondaryTextField( placeholder: "Search for locations or servers", text: $viewModel.searchText ) + .autocorrectionDisabled() .focused($focusSearchField) .padding(.horizontal) .transition(.move(edge: .top).combined(with: .opacity)) @@ -78,6 +81,7 @@ struct SelectLocationView<ViewModel>: View where ViewModel: SelectLocationViewMo ExitLocationView( viewModel: viewModel, context: $viewModel.exitContext, + isSearching: focusSearchField, onScrollOffsetChange: { prevScrollOffset, scrollOffset in @@ -93,6 +97,7 @@ struct SelectLocationView<ViewModel>: View where ViewModel: SelectLocationViewMo case .entry: EntryLocationView( viewModel: viewModel, + isSearching: focusSearchField, onScrollOffsetChange: { prevScrollOffset, scrollOffset in expandOrCollapseHeader( prevScrollOffset: prevScrollOffset, |
