Skip to content

Commit

Permalink
Create view with custom lists to edit
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Mar 14, 2024
1 parent 6410c59 commit 2d8ad57
Show file tree
Hide file tree
Showing 25 changed files with 645 additions and 198 deletions.
31 changes: 7 additions & 24 deletions ios/MullvadSettings/CustomListRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,25 @@ public enum CustomRelayListError: LocalizedError, Equatable {
}

public struct CustomListRepository: CustomListRepositoryProtocol {
public var publisher: AnyPublisher<[CustomList], Never> {
passthroughSubject.eraseToAnyPublisher()
}

private let logger = Logger(label: "CustomListRepository")
private let passthroughSubject = PassthroughSubject<[CustomList], Never>()

private let settingsParser: SettingsParser = {
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}()

public init() {}

public func create(_ name: String, locations: [RelayLocation]) throws -> CustomList {
public func save(list: CustomList) throws {
var lists = fetchAll()
if lists.contains(where: { $0.name == name }) {

if let index = lists.firstIndex(where: { $0.id == list.id }) {
lists[index] = list
try write(lists)
} else if lists.contains(where: { $0.name == list.name }) {
throw CustomRelayListError.duplicateName
} else {
let item = CustomList(id: UUID(), name: name, locations: locations)
lists.append(item)
lists.append(list)
try write(lists)
return item
}
}

Expand All @@ -72,18 +69,6 @@ public struct CustomListRepository: CustomListRepositoryProtocol {
public func fetchAll() -> [CustomList] {
(try? read()) ?? []
}

public func update(_ list: CustomList) {
do {
var lists = fetchAll()
if let index = lists.firstIndex(where: { $0.id == list.id }) {
lists[index] = list
try write(lists)
}
} catch {
logger.error(error: error)
}
}
}

extension CustomListRepository {
Expand All @@ -97,7 +82,5 @@ extension CustomListRepository {
let data = try settingsParser.produceUnversionedPayload(list)

try SettingsManager.store.write(data, for: .customRelayLists)

passthroughSubject.send(list)
}
}
15 changes: 3 additions & 12 deletions ios/MullvadSettings/CustomListRepositoryProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ import Combine
import Foundation
import MullvadTypes
public protocol CustomListRepositoryProtocol {
/// Publisher that propagates a snapshot of persistent store upon modifications.
var publisher: AnyPublisher<[CustomList], Never> { get }

/// Persist modified custom list locating existing entry by id.
/// - Parameter list: persistent custom list model.
func update(_ list: CustomList)
/// Save a custom list. If the list doesn't already exist, it must have a unique name.
/// - Parameter list: a custom list.
func save(list: CustomList) throws

/// Delete custom list by id.
/// - Parameter id: an access method id.
Expand All @@ -26,12 +23,6 @@ public protocol CustomListRepositoryProtocol {
/// - Returns: a persistent custom list model upon success, otherwise `nil`.
func fetch(by id: UUID) -> CustomList?

/// Create a custom list by unique name.
/// - Parameter name: a custom list name.
/// - Parameter locations: locations in a custom list.
/// - Returns: a persistent custom list model upon success, otherwise throws `Error`.
func create(_ name: String, locations: [RelayLocation]) throws -> CustomList

/// Fetch all custom list.
/// - Returns: all custom list model .
func fetchAll() -> [CustomList]
Expand Down
25 changes: 13 additions & 12 deletions ios/MullvadTypes/RelayConstraints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible
public var filter: RelayConstraint<RelayFilter>

// Added in 2024.1
public var locations: RelayConstraint<RelayLocations>
// Changed from RelayLocations to UserSelectedRelays in 2024.3
public var locations: RelayConstraint<UserSelectedRelays>

public var debugDescription: String {
"RelayConstraints { locations: \(locations), port: \(port) }"
"RelayConstraints { locations: \(locations), port: \(port), filter: \(filter) }"
}

public init(
locations: RelayConstraint<RelayLocations> = .only(RelayLocations(locations: [.country("se")])),
locations: RelayConstraint<UserSelectedRelays> = .only(UserSelectedRelays(locations: [.country("se")])),
port: RelayConstraint<UInt16> = .any,
filter: RelayConstraint<RelayFilter> = .any
) {
Expand All @@ -53,27 +54,27 @@ public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible
filter = try container.decodeIfPresent(RelayConstraint<RelayFilter>.self, forKey: .filter) ?? .any

// Added in 2024.1
locations = try container.decodeIfPresent(RelayConstraint<RelayLocations>.self, forKey: .locations)
?? Self.migrateLocations(decoder: decoder)
?? .only(RelayLocations(locations: [.country("se")]))
locations = try container.decodeIfPresent(RelayConstraint<UserSelectedRelays>.self, forKey: .locations)
?? Self.migrateRelayLocation(decoder: decoder)
?? .only(UserSelectedRelays(locations: [.country("se")]))
}
}

extension RelayConstraints {
private static func migrateLocations(decoder: Decoder) -> RelayConstraint<RelayLocations>? {
private static func migrateRelayLocation(decoder: Decoder) -> RelayConstraint<UserSelectedRelays>? {
let container = try? decoder.container(keyedBy: CodingKeys.self)

guard
let location = try? container?.decodeIfPresent(RelayConstraint<RelayLocation>.self, forKey: .location)
let relay = try? container?.decodeIfPresent(RelayConstraint<RelayLocation>.self, forKey: .location)
else {
return nil
}

switch location {
return switch relay {
case .any:
return .any
case let .only(location):
return .only(RelayLocations(locations: [location]))
.any
case let .only(relay):
.only(UserSelectedRelays(locations: [relay]))
}
}
}
25 changes: 25 additions & 0 deletions ios/MullvadTypes/RelayLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,31 @@ public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible {
}
}

public struct UserSelectedRelays: Codable, Equatable {
public let locations: [RelayLocation]
public let customListSelection: CustomListSelection?

public init(locations: [RelayLocation], customListSelection: CustomListSelection? = nil) {
self.locations = locations
self.customListSelection = customListSelection
}
}

extension UserSelectedRelays {
public struct CustomListSelection: Codable, Equatable {
/// The ID of the custom list that the selected relays belong to.
public let listId: UUID
/// Whether the selected relays are subnodes or the custom list itself.
public let isList: Bool

public init(listId: UUID, isList: Bool) {
self.listId = listId
self.isList = isList
}
}
}

@available(*, deprecated, message: "Use UserSelectedRelays instead.")
public struct RelayLocations: Codable, Equatable {
public let locations: [RelayLocation]
public let customListId: UUID?
Expand Down
12 changes: 12 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,15 @@
7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */; };
7A9FA1422A2E3306000B728D /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1412A2E3306000B728D /* CheckboxView.swift */; };
7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */; };
7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; };
7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */; };
7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */; };
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */; };
7ABCA5B32A9349F20044A708 /* Routing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; };
7ABCA5B42A9349F20044A708 /* Routing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7ABCA5B72A9353C60044A708 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; };
7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */; };
7ABFB09E2BA316220074A49E /* RelayConstraintsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */; };
7AC8A3AE2ABC6FBB00DC4939 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */; };
7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA22A96302700DD6A34 /* TermsOfServiceCoordinator.swift */; };
7AD0AA1C2AD6A63F00119E10 /* PacketTunnelActorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */; };
Expand Down Expand Up @@ -1815,9 +1818,12 @@
7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelCoordinator.swift; sourceTree = "<group>"; };
7A9FA1412A2E3306000B728D /* CheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxView.swift; sourceTree = "<group>"; };
7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckableSettingsCell.swift; sourceTree = "<group>"; };
7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = "<group>"; };
7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListCoordinator.swift; sourceTree = "<group>"; };
7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTests.swift; sourceTree = "<group>"; };
7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideInteractor.swift; sourceTree = "<group>"; };
7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = "<group>"; };
7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = "<group>"; };
7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorProtocol.swift; sourceTree = "<group>"; };
7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2923,6 +2929,7 @@
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */,
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */,
A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */,
7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */,
584B26F3237434D00073B10E /* RelaySelectorTests.swift */,
A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */,
A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */,
Expand Down Expand Up @@ -3485,6 +3492,8 @@
7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */,
7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */,
7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */,
7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */,
7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */,
);
path = CustomLists;
sourceTree = "<group>";
Expand Down Expand Up @@ -4821,6 +4830,7 @@
F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */,
F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */,
7ABFB09E2BA316220074A49E /* RelayConstraintsTests.swift in Sources */,
F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */,
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */,
A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */,
Expand Down Expand Up @@ -5071,6 +5081,7 @@
58FF9FE22B075BA600E4C97D /* EditAccessMethodSectionIdentifier.swift in Sources */,
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */,
7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */,
7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */,
7A9CCCB72A96302800DD6A34 /* RevokedCoordinator.swift in Sources */,
7A6389F82B864CDF008E77E1 /* LocationNode.swift in Sources */,
587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */,
Expand Down Expand Up @@ -5231,6 +5242,7 @@
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */,
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */,
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */,
7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */,
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import UIKit

class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
let navigationController: UINavigationController
let customListInteractor: CustomListInteractorProtocol
let interactor: CustomListInteractorProtocol

var presentedViewController: UIViewController {
navigationController
Expand All @@ -23,10 +23,10 @@ class AddCustomListCoordinator: Coordinator, Presentable, Presenting {

init(
navigationController: UINavigationController,
customListInteractor: CustomListInteractorProtocol
interactor: CustomListInteractorProtocol
) {
self.navigationController = navigationController
self.customListInteractor = customListInteractor
self.interactor = interactor
}

func start() {
Expand All @@ -35,7 +35,7 @@ class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
)

let controller = CustomListViewController(
interactor: customListInteractor,
interactor: interactor,
subject: subject,
alertPresenter: AlertPresenter(context: self)
)
Expand All @@ -55,16 +55,23 @@ class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
comment: ""
)

controller.navigationItem.leftBarButtonItem = UIBarButtonItem(
systemItem: .cancel,
primaryAction: UIAction(handler: { _ in
self.didFinish?()
})
)

navigationController.pushViewController(controller, animated: false)
}
}

extension AddCustomListCoordinator: CustomListViewControllerDelegate {
func customListDidSave() {
func customListDidSave(_ list: CustomList) {
didFinish?()
}

func customListDidDelete() {
func customListDidDelete(_ list: CustomList) {
// No op.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@
import MullvadSettings

protocol CustomListInteractorProtocol {
func createCustomList(viewModel: CustomListViewModel) throws
func updateCustomList(viewModel: CustomListViewModel)
func deleteCustomList(id: UUID)
func fetchAll() -> [CustomList]
func save(viewModel: CustomListViewModel) throws
func delete(id: UUID)
}

struct CustomListInteractor: CustomListInteractorProtocol {
let repository: CustomListRepositoryProtocol

func createCustomList(viewModel: CustomListViewModel) throws {
try _ = repository.create(viewModel.name, locations: viewModel.locations)
func fetchAll() -> [CustomList] {
repository.fetchAll()
}

func updateCustomList(viewModel: CustomListViewModel) {
repository.update(viewModel.customList)
func save(viewModel: CustomListViewModel) throws {
try repository.save(list: viewModel.customList)
}

func deleteCustomList(id: UUID) {
func delete(id: UUID) {
repository.delete(id: id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import MullvadSettings
import UIKit

protocol CustomListViewControllerDelegate: AnyObject {
func customListDidSave()
func customListDidDelete()
func customListDidSave(_ list: CustomList)
func customListDidDelete(_ list: CustomList)
func showLocations()
}

Expand Down Expand Up @@ -91,13 +91,6 @@ class CustomListViewController: UIViewController {
}

private func configureNavigationItem() {
navigationItem.leftBarButtonItem = UIBarButtonItem(
systemItem: .cancel,
primaryAction: UIAction(handler: { _ in
self.dismiss(animated: true)
})
)

navigationItem.rightBarButtonItem = saveBarButton
}

Expand Down Expand Up @@ -149,8 +142,8 @@ class CustomListViewController: UIViewController {

private func onSave() {
do {
try interactor.createCustomList(viewModel: subject.value)
delegate?.customListDidSave()
try interactor.save(viewModel: subject.value)
delegate?.customListDidSave(subject.value.customList)
} catch {
validationErrors.insert(.name)
dataSourceConfiguration?.set(validationErrors: validationErrors)
Expand Down Expand Up @@ -182,9 +175,8 @@ class CustomListViewController: UIViewController {
),
style: .destructive,
handler: {
self.interactor.deleteCustomList(id: self.subject.value.id)
self.dismiss(animated: true)
self.delegate?.customListDidDelete()
self.interactor.delete(id: self.subject.value.id)
self.delegate?.customListDidDelete(self.subject.value.customList)
}
),
AlertAction(
Expand Down
Loading

0 comments on commit 2d8ad57

Please sign in to comment.