Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor LocationDataSource for custom lists #5778

Merged
merged 2 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ios/MullvadTypes/RelayLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible {
}

/// A list of `RelayLocation` items preceding the given one in the relay tree
public var ascendants: [RelayLocation] {
public var ancestors: [RelayLocation] {
switch self {
case let .hostname(country, city, _):
return [.country(country), .city(country, city)]
Expand Down
28 changes: 28 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,13 @@
F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE562B7376C6003F4EDB /* CustomListRepository.swift */; };
F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE592B7376F4003F4EDB /* CustomList.swift */; };
F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */; };
F050AE4C2B70D5A7003F4EDB /* SelectLocationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */; };
F050AE4E2B70D7F8003F4EDB /* LocationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */; };
F050AE502B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */; };
F050AE522B70DFC0003F4EDB /* SelectLocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */; };
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */; };
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */; };
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */; };
F05F39942B21C6C6006E60A7 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
Expand Down Expand Up @@ -1898,6 +1905,13 @@
F050AE562B7376C6003F4EDB /* CustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepository.swift; sourceTree = "<group>"; };
F050AE592B7376F4003F4EDB /* CustomList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomList.swift; sourceTree = "<group>"; };
F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryTests.swift; sourceTree = "<group>"; };
F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNode.swift; sourceTree = "<group>"; };
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCellViewModel.swift; sourceTree = "<group>"; };
F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNodeProtocol.swift; sourceTree = "<group>"; };
F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationSection.swift; sourceTree = "<group>"; };
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSourceProtocol.swift; sourceTree = "<group>"; };
F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllLocationDataSource.swift; sourceTree = "<group>"; };
F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsDataSource.swift; sourceTree = "<group>"; };
F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; };
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; };
F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2324,9 +2338,16 @@
583FE01729C196F3006E85F9 /* SelectLocation */ = {
isa = PBXGroup;
children = (
F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */,
F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */,
58435AC129CB2A350099C71B /* LocationCellFactory.swift */,
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */,
583DA21325FA4B5C00318683 /* LocationDataSource.swift */,
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */,
5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */,
F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */,
F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */,
5888AD86227B17950051EB06 /* SelectLocationViewController.swift */,
);
path = SelectLocation;
Expand Down Expand Up @@ -4995,6 +5016,7 @@
58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */,
7A1A26472A29CF0800B978AA /* RelayFilterDataSource.swift in Sources */,
5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */,
F050AE4E2B70D7F8003F4EDB /* LocationCellViewModel.swift in Sources */,
58CEB30C2AFD586600E6E088 /* DynamicBackgroundConfiguration.swift in Sources */,
587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */,
5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */,
Expand All @@ -5008,6 +5030,7 @@
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */,
7A9CCCC12A96302800DD6A34 /* AccountCoordinator.swift in Sources */,
F050AE502B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */,
F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */,
Expand Down Expand Up @@ -5049,6 +5072,7 @@
7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */,
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */,
58FF9FE02B075ABC00E4C97D /* EditAccessMethodViewController.swift in Sources */,
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */,
F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */,
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */,
Expand Down Expand Up @@ -5081,6 +5105,7 @@
063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */,
58DF28A52417CB4B00E836B0 /* StorePaymentManager.swift in Sources */,
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */,
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */,
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */,
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */,
Expand Down Expand Up @@ -5186,7 +5211,9 @@
584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */,
5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */,
586C0D912B03D8A400E7CDD7 /* AccessMethodHeaderFooterReuseIdentifier.swift in Sources */,
F050AE4C2B70D5A7003F4EDB /* SelectLocationNode.swift in Sources */,
7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */,
F050AE522B70DFC0003F4EDB /* SelectLocationSection.swift in Sources */,
063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */,
A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
Expand All @@ -5206,6 +5233,7 @@
5827B0BF2B14B37D00CCBBA1 /* Publisher+PreviousValue.swift in Sources */,
7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */,
5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */,
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */,
7A5869A82B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift in Sources */,
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */,
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// AllLocationDataSource.swift
// MullvadVPN
//
// Created by Mojgan on 2024-02-07.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadREST
import MullvadTypes

class AllLocationDataSource: LocationDataSourceProtocol {
var nodeByLocation = [RelayLocation: SelectLocationNode]()
private var locationList = [RelayLocation]()

func search(by text: String) -> [RelayLocation] {
guard !text.isEmpty else {
return locationList
}

var filteredLocations: [RelayLocation] = []
locationList.forEach { location in
guard let countryNode = nodeByLocation[location] else { return }
countryNode.showsChildren = false

if countryNode.displayName.fuzzyMatch(text) {
filteredLocations.append(countryNode.location)
}

countryNode.children.forEach { cityNode in
cityNode.showsChildren = false

let relaysContainSearchString = cityNode.children
.contains(where: { $0.displayName.fuzzyMatch(text) })

if cityNode.displayName.fuzzyMatch(text) || relaysContainSearchString {
if !filteredLocations.contains(countryNode.location) {
filteredLocations.append(countryNode.location)
}

filteredLocations.append(cityNode.location)
countryNode.showsChildren = true

if relaysContainSearchString {
filteredLocations.append(contentsOf: cityNode.children.map { $0.location })
cityNode.showsChildren = true
}
}
}
}

return filteredLocations
}

func reload(
_ response: REST.ServerRelaysResponse,
relays: [REST.ServerRelay]
) -> [RelayLocation] {
nodeByLocation.removeAll()
let rootNode = self.makeRootNode(name: SelectLocationSection.allLocations.description)

for relay in relays {
guard case let .city(countryCode, cityCode) = RelayLocation(dashSeparatedString: relay.location),
let serverLocation = response.locations[relay.location] else { continue }

let relayLocation = RelayLocation.hostname(countryCode, cityCode, relay.hostname)

for ancestorOrSelf in relayLocation.ancestors + [relayLocation] {
guard !nodeByLocation.keys.contains(ancestorOrSelf) else {
continue
}

// Maintain the `showsChildren` state when transitioning between relay lists
let wasShowingChildren = nodeByLocation[ancestorOrSelf]?.showsChildren ?? false

let node = createNode(
root: rootNode,
ancestorOrSelf: ancestorOrSelf,
serverLocation: serverLocation,
relay: relay,
wasShowingChildren: wasShowingChildren
)
nodeByLocation[ancestorOrSelf] = node
}
}

rootNode.sortChildrenRecursive()
rootNode.computeActiveChildrenRecursive()
locationList = rootNode.flatRelayLocationList()
return locationList
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// CustomListsDataSource.swift
// MullvadVPN
//
// Created by Mojgan on 2024-02-08.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadREST
import MullvadTypes

class CustomListsDataSource: LocationDataSourceProtocol {
var nodeByLocation = [RelayLocation: SelectLocationNode]()
private var locationList = [RelayLocation]()

func search(by text: String) -> [RelayLocation] {
[]
}

func reload(
_ response: REST.ServerRelaysResponse,
relays: [REST.ServerRelay]
) -> [RelayLocation] {
locationList
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@ import MullvadTypes
import UIKit

protocol LocationCellEventHandler {
func collapseCell(for item: RelayLocation)
func toggleCell(for item: LocationCellViewModel)
func node(for item: LocationCellViewModel) -> SelectLocationNode?
}

final class LocationCellFactory: CellFactoryProtocol {
var nodeByLocation = [RelayLocation: LocationDataSource.Node]()
var delegate: LocationCellEventHandler?
let tableView: UITableView
let reuseIdentifier: String

init(tableView: UITableView, nodeByLocation: [RelayLocation: LocationDataSource.Node]) {
init(
tableView: UITableView,
reuseIdentifier: String
) {
self.tableView = tableView
self.nodeByLocation = nodeByLocation
self.reuseIdentifier = reuseIdentifier
}

func makeCell(for item: RelayLocation, indexPath: IndexPath) -> UITableViewCell {
func makeCell(for item: LocationCellViewModel, indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: LocationDataSource.CellReuseIdentifiers.locationCell.rawValue,
withIdentifier: reuseIdentifier,
for: indexPath
)

Expand All @@ -34,17 +38,17 @@ final class LocationCellFactory: CellFactoryProtocol {
return cell
}

func configureCell(_ cell: UITableViewCell, item: RelayLocation, indexPath: IndexPath) {
func configureCell(_ cell: UITableViewCell, item: LocationCellViewModel, indexPath: IndexPath) {
guard let cell = cell as? SelectLocationCell,
let node = nodeByLocation[item] else { return }
let node = delegate?.node(for: item) else { return }

cell.accessibilityIdentifier = node.location.stringRepresentation
cell.isDisabled = !node.isActive
cell.locationLabel.text = node.displayName
cell.showsCollapseControl = node.isCollapsible
cell.isExpanded = node.showsChildren
cell.didCollapseHandler = { [weak self] _ in
self?.delegate?.collapseCell(for: item)
self?.delegate?.toggleCell(for: item)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// LocationCellViewModel.swift
// MullvadVPN
//
// Created by Mojgan on 2024-02-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import MullvadTypes

struct LocationCellViewModel: Hashable {
let group: SelectLocationSection
let location: RelayLocation
}
Loading
Loading