Skip to content

Commit 43aa6eb

Browse files
mojganiikl
authored andcommitted
Refactor RelayFilterDataSource
1 parent 080e9aa commit 43aa6eb

15 files changed

+439
-413
lines changed

ios/MullvadREST/Relay/RelayCandidates.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
77
//
88

9-
public struct RelayCandidates {
9+
public struct RelayCandidates: Equatable {
1010
public let entryRelays: [RelayWithLocation<REST.ServerRelay>]?
1111
public let exitRelays: [RelayWithLocation<REST.ServerRelay>]
1212
public init(

ios/MullvadREST/Relay/RelaySelector.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public enum RelaySelector {
1515
// MARK: - public
1616

1717
/// Determines whether a `REST.ServerRelay` satisfies the given relay filter.
18-
static func relayMatchesFilter(_ relay: AnyRelay, filter: RelayFilter) -> Bool {
18+
public static func relayMatchesFilter(_ relay: AnyRelay, filter: RelayFilter) -> Bool {
1919
if case let .only(providers) = filter.providers, providers.contains(relay.provider) == false {
2020
return false
2121
}

ios/MullvadREST/Relay/RelayWithLocation.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ public struct RelayWithLocation<T: AnyRelay> {
6060
}
6161
}
6262

63-
extension RelayWithLocation: Equatable {
63+
extension RelayWithLocation: Hashable {
6464
public static func == (lhs: RelayWithLocation<T>, rhs: RelayWithLocation<T>) -> Bool {
6565
lhs.relay.hostname == rhs.relay.hostname
6666
}
67+
68+
public func hash(into hasher: inout Hasher) {
69+
hasher.combine(relay.hostname)
70+
}
6771
}

ios/MullvadTypes/RelayConstraint.swift

+3-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import Foundation
1010

1111
private let anyConstraint = "any"
1212

13-
public enum RelayConstraint<T>: Codable, Equatable,
14-
CustomDebugStringConvertible where T: Codable & Equatable {
13+
public enum RelayConstraint<T>: Codable, Equatable, CustomDebugStringConvertible, Sendable
14+
where T: Codable & Equatable & Sendable {
1515
case any
1616
case only(T)
1717

@@ -34,7 +34,7 @@ public enum RelayConstraint<T>: Codable, Equatable,
3434
return output
3535
}
3636

37-
private struct OnlyRepr: Codable {
37+
private struct OnlyRepr: Codable, Sendable {
3838
var only: T
3939
}
4040

@@ -46,7 +46,6 @@ public enum RelayConstraint<T>: Codable, Equatable,
4646
self = .any
4747
} else {
4848
let onlyVariant = try container.decode(OnlyRepr.self)
49-
5049
self = .only(onlyVariant.only)
5150
}
5251
}

ios/MullvadTypes/RelayFilter.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import Foundation
1010

11-
public struct RelayFilter: Codable, Equatable {
12-
public enum Ownership: Codable {
11+
public struct RelayFilter: Codable, Equatable, Sendable {
12+
public enum Ownership: Codable, Sendable {
1313
case any
1414
case owned
1515
case rented

ios/MullvadVPN.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,7 @@
928928
F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; };
929929
F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; };
930930
F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; };
931+
F017F8E02D78AC020076EC01 /* RelayFilterDataSourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F017F8DF2D78ABE90076EC01 /* RelayFilterDataSourceItem.swift */; };
931932
F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01DAE322C2B032A00521E46 /* RelaySelection.swift */; };
932933
F022EBA62CF0C6AE009484B9 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
933934
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; };
@@ -2346,6 +2347,7 @@
23462347
F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = "<group>"; };
23472348
F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderStub.swift; sourceTree = "<group>"; };
23482349
F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodIterator.swift; sourceTree = "<group>"; };
2350+
F017F8DF2D78ABE90076EC01 /* RelayFilterDataSourceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterDataSourceItem.swift; sourceTree = "<group>"; };
23492351
F01DAE322C2B032A00521E46 /* RelaySelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = "<group>"; };
23502352
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = "<group>"; };
23512353
F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededViewController.swift; sourceTree = "<group>"; };
@@ -4319,6 +4321,7 @@
43194321
F0B583D32D6DCE0D007F5AE4 /* FilterDescriptor.swift */,
43204322
7A1A26482A29D48A00B978AA /* RelayFilterCellFactory.swift */,
43214323
7A1A26462A29CF0800B978AA /* RelayFilterDataSource.swift */,
4324+
F017F8DF2D78ABE90076EC01 /* RelayFilterDataSourceItem.swift */,
43224325
7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */,
43234326
7A1A26442A29CEF700B978AA /* RelayFilterViewController.swift */,
43244327
7AF9BE8D2A331C7B00DBFEDB /* RelayFilterViewModel.swift */,
@@ -6385,6 +6388,7 @@
63856388
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */,
63866389
586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */,
63876390
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
6391+
F017F8E02D78AC020076EC01 /* RelayFilterDataSourceItem.swift in Sources */,
63886392
586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */,
63896393
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
63906394
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,

ios/MullvadVPN/View controllers/RelayFilter/FilterDescriptor.swift

+33-38
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,32 @@ struct FilterDescriptor {
1313
let settings: LatestTunnelSettings
1414

1515
var isEnabled: Bool {
16-
let exitCount = relayFilterResult.exitRelays.count
17-
let entryCount = relayFilterResult.entryRelays?.count ?? 0
18-
let totalcount = exitCount + entryCount
16+
// Check if multihop is enabled via settings
1917
let isMultihopEnabled = settings.tunnelMultihopState.isEnabled
20-
return (isMultihopEnabled && totalcount > 1) || (!isMultihopEnabled && totalcount > 0)
18+
let isSmartRoutingEnabled = settings.daita.isAutomaticRouting
19+
20+
/// Closure to check if there are enough relays available for multihoping
21+
let hasSufficientRelays: () -> Bool = {
22+
(relayFilterResult.entryRelays ?? []).count >= 1 &&
23+
relayFilterResult.exitRelays.count >= 1 &&
24+
numberOfServers > 1
25+
}
26+
27+
if isMultihopEnabled {
28+
// Multihop mode requires at least one entry relay, one exit relay,
29+
// and more than one unique server.
30+
return hasSufficientRelays()
31+
} else if isSmartRoutingEnabled {
32+
// Smart Routing mode: Enabled only if there is NO daita server in the exit relays
33+
let isSmartRoutingNeeded = !relayFilterResult.exitRelays.contains { $0.relay.daita == true }
34+
return isSmartRoutingNeeded ? hasSufficientRelays() : true
35+
} else {
36+
// Single-hop mode: The filter is enabled if at least one available exit relay exists.
37+
return !relayFilterResult.exitRelays.isEmpty
38+
}
2139
}
2240

2341
var title: String {
24-
let exitCount = relayFilterResult.exitRelays.count
25-
let entryCount = relayFilterResult.entryRelays?.count ?? 0
2642
guard isEnabled else {
2743
return NSLocalizedString(
2844
"RELAY_FILTER_BUTTON_TITLE",
@@ -31,29 +47,17 @@ struct FilterDescriptor {
3147
comment: ""
3248
)
3349
}
34-
return createTitleForAvailableServers(
35-
entryCount: entryCount,
36-
exitCount: exitCount,
37-
isMultihopEnabled: settings.tunnelMultihopState.isEnabled,
38-
isDirectOnly: settings.daita.isDirectOnly
39-
)
50+
return createTitleForAvailableServers()
4051
}
4152

4253
var description: String {
43-
guard settings.daita.isDirectOnly else {
44-
return settings.daita.daitaState.isEnabled
45-
? NSLocalizedString(
46-
"RELAY_FILTER_BUTTON_DESCRIPTION",
47-
tableName: "RelayFilter",
48-
value: "DAITA is enabled, affecting your filters.",
49-
comment: ""
50-
)
51-
: ""
54+
guard settings.daita.daitaState.isEnabled else {
55+
return ""
5256
}
5357
return NSLocalizedString(
5458
"RELAY_FILTER_BUTTON_DESCRIPTION",
5559
tableName: "RelayFilter",
56-
value: "Direct only DAITA is enabled, affecting your filters.",
60+
value: "When using DAITA, one provider with DAITA-enabled servers is required.",
5761
comment: ""
5862
)
5963
}
@@ -63,23 +67,14 @@ struct FilterDescriptor {
6367
self.relayFilterResult = relayFilterResult
6468
}
6569

66-
private func createTitleForAvailableServers(
67-
entryCount: Int,
68-
exitCount: Int,
69-
isMultihopEnabled: Bool,
70-
isDirectOnly: Bool
71-
) -> String {
72-
let displayNumber: (Int) -> String = { number in
73-
number > 100 ? "99+" : "\(number)"
74-
}
70+
private var numberOfServers: Int {
71+
Set(relayFilterResult.entryRelays ?? []).union(relayFilterResult.exitRelays).count
72+
}
7573

76-
if isMultihopEnabled && isDirectOnly {
77-
return String(
78-
format: "Show %@ entry & %@ exit servers",
79-
displayNumber(entryCount),
80-
displayNumber(exitCount)
81-
)
74+
private func createTitleForAvailableServers() -> String {
75+
let displayNumber: (Int) -> String = { number in
76+
number >= 100 ? "99+" : "\(number)"
8277
}
83-
return String(format: "Show %@ servers", displayNumber(exitCount))
78+
return String(format: "Show %@ servers", displayNumber(numberOfServers))
8479
}
8580
}

ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift

+39-40
Original file line numberDiff line numberDiff line change
@@ -13,78 +13,77 @@ struct RelayFilterCellFactory: @preconcurrency CellFactoryProtocol {
1313
let tableView: UITableView
1414

1515
func makeCell(for item: RelayFilterDataSource.Item, indexPath: IndexPath) -> UITableViewCell {
16-
let cell = tableView.dequeueReusableCell(withIdentifier: item.reuseIdentifier.rawValue, for: indexPath)
16+
let cell = tableView.dequeueReusableCell(
17+
withIdentifier: RelayFilterDataSource.CellReuseIdentifiers.allCases[indexPath.section].rawValue,
18+
for: indexPath
19+
)
1720
configureCell(cell, item: item, indexPath: indexPath)
1821

1922
return cell
2023
}
2124

2225
func configureCell(_ cell: UITableViewCell, item: RelayFilterDataSource.Item, indexPath: IndexPath) {
23-
switch item {
26+
switch item.type {
2427
case .ownershipAny, .ownershipOwned, .ownershipRented:
25-
configureOwnershipCell(cell, item: item)
28+
configureOwnershipCell(cell as? SelectableSettingsCell, item: item)
2629
case .allProviders, .provider:
27-
configureProviderCell(cell, item: item)
30+
configureProviderCell(cell as? CheckableSettingsCell, item: item)
2831
}
2932
}
3033

31-
private func configureOwnershipCell(_ cell: UITableViewCell, item: RelayFilterDataSource.Item) {
32-
guard let cell = cell as? SelectableSettingsCell else { return }
33-
34-
var title = ""
35-
switch item {
36-
case .ownershipAny:
37-
title = "Any"
38-
cell.setAccessibilityIdentifier(.ownershipAnyCell)
39-
case .ownershipOwned:
40-
title = "Mullvad owned only"
41-
cell.setAccessibilityIdentifier(.ownershipMullvadOwnedCell)
42-
case .ownershipRented:
43-
title = "Rented only"
44-
cell.setAccessibilityIdentifier(.ownershipRentedCell)
45-
default:
46-
assertionFailure("Item mismatch. Got: \(item)")
47-
}
34+
private func configureOwnershipCell(_ cell: SelectableSettingsCell?, item: RelayFilterDataSource.Item) {
35+
guard let cell = cell else { return }
4836

4937
cell.titleLabel.text = NSLocalizedString(
5038
"RELAY_FILTER_CELL_LABEL",
5139
tableName: "Relay filter ownership cell",
52-
value: title,
40+
value: item.name,
5341
comment: ""
5442
)
5543

44+
let accessibilityIdentifier: AccessibilityIdentifier
45+
switch item.type {
46+
case .ownershipAny:
47+
accessibilityIdentifier = .ownershipAnyCell
48+
case .ownershipOwned:
49+
accessibilityIdentifier = .ownershipMullvadOwnedCell
50+
case .ownershipRented:
51+
accessibilityIdentifier = .ownershipRentedCell
52+
default:
53+
assertionFailure("Unexpected ownership item: \(item)")
54+
return
55+
}
56+
57+
cell.setAccessibilityIdentifier(accessibilityIdentifier)
5658
cell.applySubCellStyling()
5759
}
5860

59-
private func configureProviderCell(_ cell: UITableViewCell, item: RelayFilterDataSource.Item) {
60-
guard let cell = cell as? CheckableSettingsCell else { return }
61-
62-
let title: String
63-
64-
switch item {
65-
case .allProviders:
66-
title = "All providers"
67-
setFontWeight(.semibold, to: cell.titleLabel)
68-
case let .provider(name):
69-
title = name
70-
setFontWeight(.regular, to: cell.titleLabel)
71-
default:
72-
title = ""
73-
assertionFailure("Item mismatch. Got: \(item)")
74-
}
61+
private func configureProviderCell(_ cell: CheckableSettingsCell?, item: RelayFilterDataSource.Item) {
62+
guard let cell = cell else { return }
63+
let alpha = item.isEnabled ? 1.0 : 0.5
7564

7665
cell.titleLabel.text = NSLocalizedString(
7766
"RELAY_FILTER_CELL_LABEL",
7867
tableName: "Relay filter provider cell",
79-
value: title,
68+
value: item.name,
8069
comment: ""
8170
)
71+
cell.detailTitleLabel.text = item.description
72+
73+
if item.type == .allProviders {
74+
setFontWeight(.semibold, to: cell.titleLabel)
75+
} else {
76+
setFontWeight(.regular, to: cell.titleLabel)
77+
}
8278

8379
cell.applySubCellStyling()
8480
cell.setAccessibilityIdentifier(.relayFilterProviderCell)
81+
cell.titleLabel.alpha = alpha
82+
cell.detailTitleLabel.alpha = alpha
83+
cell.detailTitleLabel.textColor = cell.titleLabel.textColor
8584
}
8685

8786
private func setFontWeight(_ weight: UIFont.Weight, to label: UILabel) {
88-
label.font = UIFont.systemFont(ofSize: label.font.pointSize, weight: .semibold)
87+
label.font = UIFont.systemFont(ofSize: label.font.pointSize, weight: weight)
8988
}
9089
}

0 commit comments

Comments
 (0)