Skip to content

Commit

Permalink
Update view model when switching between entry and exit location
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed May 8, 2024
1 parent 1d83511 commit 0438746
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 58 deletions.
4 changes: 4 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; };
7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; };
7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; };
7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */; };
7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; };
7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; };
7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; };
Expand Down Expand Up @@ -1772,6 +1773,7 @@
7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = "<group>"; };
7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = "<group>"; };
7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = "<group>"; };
7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = "<group>"; };
7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = "<group>"; };
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Scoping.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2647,6 +2649,7 @@
F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */,
5888AD86227B17950051EB06 /* LocationViewController.swift */,
7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */,
7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */,
);
path = SelectLocation;
sourceTree = "<group>";
Expand Down Expand Up @@ -5503,6 +5506,7 @@
5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */,
7A28826D2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift in Sources */,
A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */,
7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */,
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */,
7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
Expand Down
18 changes: 15 additions & 3 deletions ios/MullvadVPN/Coordinators/LocationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting {
func start() {
let locationViewControllerWrapper = LocationViewControllerWrapper(
customListRepository: customListRepository,
selectedRelays: tunnelManager.settings.relayConstraints.locations.value
selectedRelays: createSelectedRelays()
)
locationViewControllerWrapper.delegate = self

Expand All @@ -75,6 +75,20 @@ class LocationCoordinator: Coordinator, Presentable, Presenting {
navigationController.pushViewController(locationViewControllerWrapper, animated: false)
}

private func createSelectedRelays() -> RelaySelection {
var selectedRelays = RelaySelection(
entry: UserSelectedRelays(locations: [.country("se")]),
exit: tunnelManager.settings.relayConstraints.locations.value,
currentContext: nil
)

#if DEBUG
selectedRelays.currentContext = .exit
#endif

return selectedRelays
}

private func makeRelayFilterCoordinator(forModalPresentation isModalPresentation: Bool)
-> RelayFilterCoordinator {
let navigationController = CustomNavigationController()
Expand Down Expand Up @@ -161,8 +175,6 @@ extension LocationCoordinator: LocationViewControllerWrapperDelegate {
tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) {
self.tunnelManager.startTunnel()
}

didFinish?(self)
}

func didUpdateFilter(filter: RelayFilter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class RelayFilterView: UIView {
contentContainer.spacing = UIMetrics.FilterView.labelSpacing

addConstrainedSubviews([contentContainer]) {
contentContainer.pinEdges(.init([.top(4), .bottom(0)]), to: self)
contentContainer.pinEdges(.init([.top(7), .bottom(0)]), to: self)
contentContainer.pinEdges(.init([.leading(4), .trailing(4)]), to: layoutMarginsGuide)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class LocationCell: UITableViewCell {
private var behavior: LocationCellBehavior = .select
private let chevronDown = UIImage(resource: .iconChevronDown)
private let chevronUp = UIImage(resource: .iconChevronUp)
private var isMultipHopSelection = false

var isDisabled = false {
didSet {
Expand Down Expand Up @@ -244,7 +245,7 @@ class LocationCell: UITableViewCell {
}

private func statusIndicatorColor() -> UIColor {
if isDisabled {
if isDisabled && !isMultipHopSelection {
return UIColor.RelayStatusIndicator.inactiveColor
} else if isHighlighted {
return UIColor.RelayStatusIndicator.highlightColor
Expand Down Expand Up @@ -318,9 +319,18 @@ extension LocationCell {
checkboxButton.isSelected = item.isSelected
checkboxButton.tintColor = item.isSelected ? .successColor : .white

setMultihopSelection(item.multihopContext)
setBehavior(behavior)
}

func setMultihopSelection(_ context: RelaySelection.MultihopContext?) {
if let context {
isMultipHopSelection = true
isDisabled = true
locationLabel.text! += " (\(context.description))"
}
}

private func setBehavior(_ newBehavior: LocationCellBehavior) {
self.behavior = newBehavior
updateLeadingImage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ struct LocationCellViewModel: Hashable {
let node: LocationNode
var indentationLevel = 0
var isSelected = false
var multihopContext: RelaySelection.MultihopContext?

func hash(into hasher: inout Hasher) {
hasher.combine(node)
hasher.combine(node.children.count)
hasher.combine(section)
hasher.combine(isSelected)
hasher.combine(indentationLevel)
hasher.combine(multihopContext)
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.node == rhs.node &&
lhs.node.children.count == rhs.node.children.count &&
lhs.section == rhs.section &&
lhs.isSelected == rhs.isSelected &&
lhs.indentationLevel == rhs.indentationLevel
lhs.multihopContext == rhs.multihopContext
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ final class LocationDataSource:
LocationDiffableDataSourceProtocol {
private var currentSearchString = ""
private var dataSources: [LocationDataSourceProtocol] = []
private var selectedItem: LocationCellViewModel?
private var selection: LocationCellViewModel?
private var multiHopSelection: LocationCellViewModel?
let tableView: UITableView
let sections: [LocationSection]

Expand Down Expand Up @@ -50,7 +51,7 @@ final class LocationDataSource:
defaultRowAnimation = .fade
}

func setRelays(_ response: REST.ServerRelaysResponse, selectedRelays: UserSelectedRelays?, filter: RelayFilter) {
func setRelays(_ response: REST.ServerRelaysResponse, selectedRelays: RelaySelection, filter: RelayFilter) {
let allLocationsDataSource =
dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource

Expand All @@ -64,7 +65,7 @@ final class LocationDataSource:
allLocationsDataSource?.reload(response, relays: relays)
customListsDataSource?.reload(allLocationNodes: allLocationsDataSource?.nodes ?? [])

mapSelectedItem(from: selectedRelays)
setSelectedRelays(selectedRelays)
filterRelays(by: currentSearchString)
}

Expand All @@ -81,8 +82,9 @@ final class LocationDataSource:
}

updateDataSnapshot(with: list, reloadExisting: !searchString.isEmpty) {
self.tableView.reloadData()
if searchString.isEmpty {
self.setSelectedItem(self.selectedItem, animated: false, completion: {
self.updateSelection(self.selection, animated: false, completion: {
self.scrollToSelectedRelay()
})
} else {
Expand All @@ -92,7 +94,7 @@ final class LocationDataSource:
}

/// Refreshes the custom list section and keeps all modifications intact (selection and expanded states).
func refreshCustomLists(selectedRelays: UserSelectedRelays?) {
func refreshCustomLists(selectedRelays: RelaySelection) {
guard let allLocationsDataSource =
dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource,
let customListsDataSource =
Expand All @@ -110,7 +112,7 @@ final class LocationDataSource:
customListsDataSource.reload(allLocationNodes: allLocationsDataSource.nodes)

// Reapply current selection.
mapSelectedItem(from: selectedRelays)
setSelectedRelays(selectedRelays)

// Reapply current search filter.
let searchResultNodes = dataSources[0].search(by: currentSearchString)
Expand All @@ -133,6 +135,16 @@ final class LocationDataSource:
], reloadExisting: true)
}

func setSelectedRelays(_ selectedRelays: RelaySelection) {
let contextScopedRelays = selectedRelays.relaysFromCurrentContext

selection = mapSelection(from: contextScopedRelays.current)
multiHopSelection = mapMultihopSelection(from: contextScopedRelays.inverted)
multiHopSelection?.multihopContext = selectedRelays.invertedContext

tableView.reloadData()
}

func scrollToSelectedRelay() {
indexPathForSelectedRelay().flatMap {
tableView.scrollToRow(at: $0, at: .middle, animated: false)
Expand All @@ -146,14 +158,14 @@ final class LocationDataSource:

// Called from `LocationDiffableDataSourceProtocol`.
func nodeShouldBeSelected(_ node: LocationNode) -> Bool {
false
false // N/A
}

private func indexPathForSelectedRelay() -> IndexPath? {
selectedItem.flatMap { indexPath(for: $0) }
selection.flatMap { indexPath(for: $0) }
}

private func mapSelectedItem(from selectedRelays: UserSelectedRelays?) {
private func mapSelection(from selectedRelays: UserSelectedRelays?) -> LocationCellViewModel? {
let allLocationsDataSource =
dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource

Expand All @@ -165,55 +177,67 @@ final class LocationDataSource:
if let customListSelection = selectedRelays.customListSelection,
let customList = customListsDataSource?.customList(by: customListSelection.listId),
let selectedNode = customListsDataSource?.node(by: selectedRelays, for: customList) {
selectedItem = LocationCellViewModel(
return LocationCellViewModel(
section: .customLists,
node: selectedNode,
indentationLevel: selectedNode.hierarchyLevel
)
// Look for a matching all locations node.
} else if let location = selectedRelays.locations.first,
let selectedNode = allLocationsDataSource?.node(by: location) {
selectedItem = LocationCellViewModel(
return LocationCellViewModel(
section: .allLocations,
node: selectedNode,
indentationLevel: selectedNode.hierarchyLevel
)
}
}

return nil
}

private func mapMultihopSelection(from selectedRelays: UserSelectedRelays?) -> LocationCellViewModel? {
// Multihop selection doesn't apply to anything but single hosts.
guard
selectedRelays?.locations.count == 1,
case .hostname = selectedRelays?.locations.first
else { return nil }

return mapSelection(from: selectedRelays)
}

private func setSelectedItem(_ item: LocationCellViewModel?, animated: Bool, completion: (() -> Void)? = nil) {
selectedItem = item
guard let selectedItem else { return }
private func updateSelection(_ item: LocationCellViewModel?, animated: Bool, completion: (() -> Void)? = nil) {
selection = item
guard let selection else { return }

let rootNode = selectedItem.node.root
let rootNode = selection.node.root

// Exit early if no changes to the node tree should be made.
guard selectedItem.node != rootNode else {
guard selection.node != rootNode else {
completion?()
return
}

// Make sure we have an index path for the selected item.
guard let indexPath = indexPath(for: LocationCellViewModel(
section: selectedItem.section,
section: selection.section,
node: rootNode
)) else { return }

// Walk tree backwards to determine which nodes should be expanded.
selectedItem.node.forEachAncestor { node in
selection.node.forEachAncestor { node in
node.showsChildren = true
}

// Construct node tree.
let nodesToAdd = recursivelyCreateCellViewModelTree(
for: rootNode,
in: selectedItem.section,
in: selection.section,
indentationLevel: 1
)

// Insert the new node tree below the selected item.
var snapshotItems = snapshot().itemIdentifiers(inSection: selectedItem.section)
var snapshotItems = snapshot().itemIdentifiers(inSection: selection.section)
snapshotItems.insert(contentsOf: nodesToAdd, at: indexPath.row + 1)

let list = sections.enumerated().map { index, section in
Expand All @@ -233,6 +257,11 @@ final class LocationDataSource:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// swiftlint:disable:next force_cast
let cell = super.tableView(tableView, cellForRowAt: indexPath) as! LocationCell

if itemIdentifier(for: indexPath)?.node == multiHopSelection?.node {
cell.setMultihopSelection(multiHopSelection?.multihopContext)
}

cell.delegate = self
return cell
}
Expand Down Expand Up @@ -271,31 +300,34 @@ extension LocationDataSource: UITableViewDelegate {
}

func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
itemIdentifier(for: indexPath)?.node.isActive ?? false
guard let item = itemIdentifier(for: indexPath) else { return false }
return (item.node != multiHopSelection?.node) && item.node.isActive
}

func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
itemIdentifier(for: indexPath)?.indentationLevel ?? 0
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let item = itemIdentifier(for: indexPath), item == selectedItem {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
if let item = itemIdentifier(for: indexPath), item == selection {
cell.setSelected(true, animated: false)
}
}

func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if let indexPath = indexPathForSelectedRelay() {
tableView.deselectRow(at: indexPath, animated: false)
selectedItem = nil
if let cell = tableView.cellForRow(at: indexPath) {
cell.setSelected(false, animated: false)
}
selection = nil
}

return indexPath
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = itemIdentifier(for: indexPath) else { return }
selectedItem = item
selection = item

var customListSelection: UserSelectedRelays.CustomListSelection?
if let topmostNode = item.node.root as? CustomListLocationNode {
Expand Down
Loading

0 comments on commit 0438746

Please sign in to comment.