diff options
| author | mojganii <mojgan.jelodar@codic.se> | 2024-03-05 10:19:11 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-03-12 08:31:23 +0100 |
| commit | 1c89cd07fb90e23dd52a5e9441a2d144e913a7c3 (patch) | |
| tree | 23fef51c6ca36fae91142eb2e4038b17e9b02acf | |
| parent | 2df44a8aaa1c4673d666941a2fa3aa6afc2bfa37 (diff) | |
| download | mullvadvpn-1c89cd07fb90e23dd52a5e9441a2d144e913a7c3.tar.xz mullvadvpn-1c89cd07fb90e23dd52a5e9441a2d144e913a7c3.zip | |
adding stub for custom lists
7 files changed, 219 insertions, 12 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 815b5a268f..f709874aca 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -848,6 +848,7 @@ F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; F09D04C02AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */; }; F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; + F0A92B3C2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A92B3B2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift */; }; F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */; }; F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; F0C3333C2B31A29C00D1A478 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; @@ -1981,6 +1982,7 @@ F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionStub.swift; sourceTree = "<group>"; }; F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionService.swift; sourceTree = "<group>"; }; F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = "<group>"; }; + F0A92B3B2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryCustomListRepository.swift; sourceTree = "<group>"; }; F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Async.swift"; sourceTree = "<group>"; }; F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; }; F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = "<group>"; }; @@ -2403,6 +2405,7 @@ children = ( F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */, F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */, + F0A92B3B2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift */, 5888AD82227B11080051EB06 /* LocationCell.swift */, 58435AC129CB2A350099C71B /* LocationCellFactory.swift */, F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */, @@ -5129,6 +5132,7 @@ 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */, 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */, E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */, + F0A92B3C2B8E44F900DC7B37 /* InMemoryCustomListRepository.swift in Sources */, 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */, 0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */, 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */, diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index d0fc0c6bc3..b92a5492d8 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -43,6 +43,14 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo */ private let secondaryNavigationContainer = RootContainerViewController() + private var customListRepository: CustomListRepositoryProtocol { + #if DEBUG + InMemoryCustomListRepository() + #else + CustomListRepository() + #endif + } + /// Posts `preferredAccountNumber` notification when user inputs the account number instead of voucher code private let preferredAccountNumberSubject = PassthroughSubject<String, Never>() @@ -710,7 +718,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo let locationCoordinator = LocationCoordinator( navigationController: navigationController, tunnelManager: tunnelManager, - relayCacheTracker: relayCacheTracker + relayCacheTracker: relayCacheTracker, + customListRepository: customListRepository ) locationCoordinator.didFinish = { [weak self] _ in diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListsDataSource.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListsDataSource.swift new file mode 100644 index 0000000000..e24a4c46dd --- /dev/null +++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListsDataSource.swift @@ -0,0 +1,103 @@ +// +// CustomListsDataSource.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-02-22. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST +import MullvadSettings +import MullvadTypes +import UIKit + +class CustomListsDataSource: LocationDataSourceProtocol { + private(set) var nodes = [LocationNode]() + var didTapEditCustomLists: (() -> Void)? + + var viewForHeader: UIView? { + LocationSectionHeaderView(configuration: LocationSectionHeaderView.Configuration( + name: LocationSection.customLists.description, + primaryAction: UIAction( + handler: { [weak self] _ in + self?.didTapEditCustomLists?() + } + ) + )) + } + + init(didTapEditCustomLists: (() -> Void)?) { + self.didTapEditCustomLists = didTapEditCustomLists + } + + var searchableNodes: [LocationNode] { + nodes.flatMap { $0.children } + } + + func reload(allLocationNodes: [LocationNode], customLists: [CustomList]) { + nodes = customLists.map { list in + let listNode = LocationListNode( + nodeName: list.name, + nodeCode: list.name.lowercased(), + locations: list.locations, + customList: list + ) + + listNode.children = list.locations.compactMap { location in + copy(location, from: allLocationNodes, withParent: listNode) + } + + listNode.forEachDescendant { _, node in + node.nodeCode = "\(listNode.nodeCode)-\(node.nodeCode)" + } + + return listNode + } + } + + func node(by locations: [RelayLocation], for customList: CustomList) -> LocationNode? { + guard let customListNode = nodes.first(where: { $0.nodeName == customList.name }) + else { return nil } + + if locations.count > 1 { + return customListNode + } else { + return switch locations.first { + case let .country(countryCode): + customListNode.nodeFor(nodeCode: "\(customListNode.nodeCode)-\(countryCode)") + case let .city(_, cityCode): + customListNode.nodeFor(nodeCode: "\(customListNode.nodeCode)-\(cityCode)") + case let .hostname(_, _, hostCode): + customListNode.nodeFor(nodeCode: "\(customListNode.nodeCode)-\(hostCode)") + case .none: + nil + } + } + } + + private func copy( + _ location: RelayLocation, + from allLocationNodes: [LocationNode], + withParent rootNode: LocationNode + ) -> LocationNode? { + let rootNode = RootNode(children: allLocationNodes) + + return switch location { + case let .country(countryCode): + rootNode + .countryFor(countryCode: countryCode)?.copy(withParent: rootNode) + + case let .city(countryCode, cityCode): + rootNode + .countryFor(countryCode: countryCode)?.copy(withParent: rootNode) + .cityFor(cityCode: cityCode) + + case let .hostname(countryCode, cityCode, hostCode): + rootNode + .countryFor(countryCode: countryCode)?.copy(withParent: rootNode) + .cityFor(cityCode: cityCode)? + .hostFor(hostCode: hostCode) + } + } +} diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift index 39bffdaab9..fefc4a6f3a 100644 --- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift @@ -7,6 +7,7 @@ // import MullvadREST +import MullvadSettings import MullvadTypes import Routing import UIKit @@ -15,6 +16,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack private let tunnelManager: TunnelManager private let relayCacheTracker: RelayCacheTracker private var cachedRelays: CachedRelays? + private var customListRepository: CustomListRepositoryProtocol let navigationController: UINavigationController @@ -42,17 +44,21 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack init( navigationController: UINavigationController, tunnelManager: TunnelManager, - relayCacheTracker: RelayCacheTracker + relayCacheTracker: RelayCacheTracker, + customListRepository: CustomListRepositoryProtocol ) { self.navigationController = navigationController self.tunnelManager = tunnelManager self.relayCacheTracker = relayCacheTracker + self.customListRepository = customListRepository } func start() { - let selectLocationViewController = LocationViewController() + let locationViewController = LocationViewController(customListRepository: customListRepository) + locationViewController.delegate = self + + locationViewController.didSelectRelays = { [weak self] locations in - selectLocationViewController.didSelectRelays = { [weak self] locations in guard let self else { return } var relayConstraints = tunnelManager.settings.relayConstraints @@ -65,7 +71,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack didFinish?(self) } - selectLocationViewController.navigateToFilter = { [weak self] in + locationViewController.navigateToFilter = { [weak self] in guard let self else { return } let coordinator = makeRelayFilterCoordinator(forModalPresentation: true) @@ -74,7 +80,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack presentChild(coordinator, animated: true) } - selectLocationViewController.didUpdateFilter = { [weak self] filter in + locationViewController.didUpdateFilter = { [weak self] filter in guard let self else { return } var relayConstraints = tunnelManager.settings.relayConstraints @@ -83,7 +89,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) } - selectLocationViewController.didFinish = { [weak self] in + locationViewController.didFinish = { [weak self] in guard let self else { return } didFinish?(self) @@ -93,12 +99,12 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack if let cachedRelays = try? relayCacheTracker.getCachedRelays() { self.cachedRelays = cachedRelays - selectLocationViewController.setCachedRelays(cachedRelays, filter: relayFilter) + locationViewController.setCachedRelays(cachedRelays, filter: relayFilter) } - selectLocationViewController.relayLocations = tunnelManager.settings.relayConstraints.locations.value + locationViewController.relayLocations = tunnelManager.settings.relayConstraints.locations.value - navigationController.pushViewController(selectLocationViewController, animated: false) + navigationController.pushViewController(locationViewController, animated: false) } private func makeRelayFilterCoordinator(forModalPresentation isModalPresentation: Bool) @@ -131,3 +137,16 @@ class LocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrack selectLocationViewController?.setCachedRelays(cachedRelays, filter: relayFilter) } } + +extension LocationCoordinator: LocationViewControllerDelegate { + func didRequestRouteToCustomLists(_ controller: LocationViewController) { + let coordinator = AddCustomListCoordinator( + navigationController: CustomNavigationController(), + customListInteractor: CustomListInteractor( + repository: customListRepository + ) + ) + coordinator.start() + presentChild(coordinator, animated: true) + } +} diff --git a/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift index a6e9e1bab0..7427b78cc9 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift @@ -9,6 +9,7 @@ import Foundation import MullvadREST import MullvadTypes +import UIKit class AllLocationDataSource: LocationDataSourceProtocol { private(set) var nodes = [LocationNode]() diff --git a/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift b/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift new file mode 100644 index 0000000000..fbc33ad071 --- /dev/null +++ b/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift @@ -0,0 +1,55 @@ +// +// InMemoryCustomListRepository.swift +// MullvadVPN +// +// Created by Mojgan on 2024-01-31. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Combine +import Foundation +import MullvadSettings +import MullvadTypes + +class InMemoryCustomListRepository: CustomListRepositoryProtocol { + var publisher: AnyPublisher<[CustomList], Never> { + passthroughSubject.eraseToAnyPublisher() + } + + private var customRelayLists: [CustomList] = [ + CustomList(id: UUID(), name: "Netflix", locations: [.city("al", "tia")]), + CustomList(id: UUID(), name: "Streaming", locations: [ + .city("us", "dal"), + .country("se"), + .city("de", "ber"), + ]), + ] + + private let passthroughSubject = PassthroughSubject<[CustomList], Never>() + + func update(_ list: CustomList) { + if let index = customRelayLists.firstIndex(where: { $0.id == list.id }) { + customRelayLists[index] = list + } + } + + func delete(id: UUID) { + if let index = customRelayLists.firstIndex(where: { $0.id == id }) { + customRelayLists.remove(at: index) + } + } + + func fetch(by id: UUID) -> CustomList? { + return customRelayLists.first(where: { $0.id == id }) + } + + func create(_ name: String, locations: [RelayLocation]) throws -> CustomList { + let item = CustomList(id: UUID(), name: name, locations: locations) + customRelayLists.append(item) + return item + } + + func fetchAll() -> [CustomList] { + customRelayLists + } +} diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index 16f4797d80..3c37884544 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -12,15 +12,21 @@ import MullvadSettings import MullvadTypes import UIKit +protocol LocationViewControllerDelegate: AnyObject { + func didRequestRouteToCustomLists(_ controller: LocationViewController) +} + final class LocationViewController: UIViewController { private let searchBar = UISearchBar() - private let tableView = UITableView() + private let tableView = UITableView(frame: .zero, style: .grouped) private let topContentView = UIStackView() private let filterView = RelayFilterView() private var dataSource: LocationDataSource? private var cachedRelays: CachedRelays? private var filter = RelayFilter() var relayLocations: RelayLocations? + weak var delegate: LocationViewControllerDelegate? + var customListRepository: CustomListRepositoryProtocol override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent @@ -35,6 +41,15 @@ final class LocationViewController: UIViewController { var didUpdateFilter: ((RelayFilter) -> Void)? var didFinish: (() -> Void)? + init(customListRepository: CustomListRepositoryProtocol) { + self.customListRepository = customListRepository + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - View lifecycle override func viewDidLoad() { @@ -108,7 +123,7 @@ final class LocationViewController: UIViewController { dataSource = LocationDataSource( tableView: tableView, allLocations: AllLocationDataSource(), - customLists: CustomListsDataSource(repository: CustomListRepository()) + customLists: CustomListsDataSource(repository: customListRepository) ) dataSource?.didSelectRelayLocations = { [weak self] locations in @@ -128,6 +143,7 @@ final class LocationViewController: UIViewController { tableView.indicatorStyle = .white tableView.keyboardDismissMode = .onDrag tableView.accessibilityIdentifier = .selectLocationTableView + tableView.sectionHeaderHeight = 56.0 } private func setUpTopContent() { |
