summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationDataSourceProtocol.swift17
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/DataSource/LocationNode.swift3
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/Views/EntryLocationView.swift3
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/Views/ExitLocationView.swift20
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/Views/LocationListItem.swift2
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/Views/LocationsListView.swift1
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/Views/RelayItemView.swift6
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/Views/SelectLocationView.swift45
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,