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

Create view with custom lists to edit #5909

Merged
merged 1 commit into from
Mar 15, 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
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
Loading