summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift
blob: d323df0f02f467d7ecda1ea0ce5e9db7619ab968 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//
//  LocationCellViewModel.swift
//  MullvadVPN
//
//  Created by Mojgan on 2024-02-05.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import MullvadTypes

struct LocationCellViewModel: Hashable, Sendable {
    let section: LocationSection
    let node: LocationNode
    var indentationLevel = 0
    var isSelected = false
    var excludedRelayTitle: String?

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

    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
    }
}

extension [LocationCellViewModel] {
    mutating func addSubNodes(from item: LocationCellViewModel, at indexPath: IndexPath) {
        let section = LocationSection.allCases[indexPath.section]
        let row = indexPath.row + 1
        item.node.showsChildren = true

        let locations = item.node.children.map {
            LocationCellViewModel(
                section: section,
                node: $0,
                indentationLevel: item.indentationLevel + 1,
                isSelected: item.isSelected
            )
        }

        if row < count {
            insert(contentsOf: locations, at: row)
        } else {
            append(contentsOf: locations)
        }
    }

    mutating func removeSubNodes(from node: LocationNode) {
        for node in node.children {
            node.showsChildren = false
            removeAll(where: { node == $0.node })

            removeSubNodes(from: node)
        }
    }
}

extension LocationCellViewModel {
    /* Exclusion of other locations in the same node tree (as the currently excluded location)
     happens when there are no more hosts in that tree that can be selected.
     We check this by doing the following, in order:
    
     1. Count hostnames in the tree. More than one means that there are other locations than
     the excluded one for the relay selector to choose from. No exclusion.
    
     2. Count hostnames in the excluded node. More than one means that there are multiple
     locations for the relay selector to choose from. No exclusion.
    
     3. Check existence of a location in the tree that matches the currently excluded location.
     No match means no exclusion.
     */
    func shouldExcludeLocation(_ excludedLocation: LocationCellViewModel?) -> Bool {
        guard let excludedLocation else {
            return false
        }

        let proxyNode = RootLocationNode(children: [node])
        let allLocations = Set(proxyNode.flattened.flatMap { $0.locations })
        let hostCount = allLocations.filter { location in
            if case .hostname = location { true } else { false }
        }.count

        // If there's more than one selectable relay in the current node we don't need
        // to show this in the location tree and can return early.
        guard hostCount == 1 else { return false }

        let proxyExcludedNode = RootLocationNode(children: [excludedLocation.node])
        let allExcludedLocations = Set(proxyExcludedNode.flattened.flatMap { $0.locations })
        let excludedHostCount = allExcludedLocations.filter { location in
            if case .hostname = location { true } else { false }
        }.count

        // If there's more than one selectable relay in the excluded node we don't need
        // to show this in the location tree and can return early.
        guard excludedHostCount == 1 else { return false }

        var containsExcludedLocation = false
        if allLocations.contains(where: { allExcludedLocations.contains($0) }) {
            containsExcludedLocation = true
        }

        // If the tree doesn't contain the excluded node we do nothing, otherwise the
        // required conditions have now all been met.
        return containsExcludedLocation
    }
}