Skip to content

Commit 83b27d7

Browse files
committed
Merge branch 'release/2.8.3'
2 parents 3aeeccc + d198171 commit 83b27d7

File tree

58 files changed

+894
-66
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+894
-66
lines changed

Cryptomator.xcodeproj/project.pbxproj

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
11E84AE6F7F105AB9A0E2EAE /* TrustedHubHostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0964E7C048AC1D59105CF75C /* TrustedHubHostsViewController.swift */; };
1011
3BCBE843A35B7D9E16795451 /* LifetimeUpgradeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EA4FC7AFEB4B45864C4FB10 /* LifetimeUpgradeViewModel.swift */; };
1112
4A03255E25A368BF00E63D7A /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */; };
1213
4A03257825A36A6900E63D7A /* VaultListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03257725A36A6900E63D7A /* VaultListViewController.swift */; };
@@ -375,6 +376,7 @@
375376
4AFD8C0F269304A700F77BA6 /* UnlockVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFD8C0E269304A700F77BA6 /* UnlockVaultViewModel.swift */; };
376377
4AFE6AA82514B65800A4A315 /* CloudPath+NameCollision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFE6AA72514B65800A4A315 /* CloudPath+NameCollision.swift */; };
377378
4AFF1BB1272C337A00F41E1B /* XCTest+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFF1BB0272C337A00F41E1B /* XCTest+Combine.swift */; };
379+
6436F6BD08894DAD03812751 /* TrustedHubHostsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46F99A7913E9584535D9F5FC /* TrustedHubHostsViewModel.swift */; };
378380
740376292587AFF70023FF53 /* libCryptomatorFileProvider.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 740375D72587AE7A0023FF53 /* libCryptomatorFileProvider.a */; };
379381
7408E6BF267783F100D7FAEA /* LocalWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7408E6BE267783F100D7FAEA /* LocalWebViewController.swift */; };
380382
7408E6C126778C7A00D7FAEA /* LocalWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7408E6C026778C7A00D7FAEA /* LocalWebViewModel.swift */; };
@@ -554,11 +556,13 @@
554556
/* End PBXCopyFilesBuildPhase section */
555557

556558
/* Begin PBXFileReference section */
559+
0964E7C048AC1D59105CF75C /* TrustedHubHostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedHubHostsViewController.swift; sourceTree = "<group>"; };
557560
1493C4C38F17D56A5AFA11AF /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Intents.strings; sourceTree = "<group>"; };
558561
153B84C203850D7414DE2A66 /* FileProviderAdapterRecoverUploadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderAdapterRecoverUploadsTests.swift; sourceTree = "<group>"; };
559562
1635F1C6B23AA6FBFF781706 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
560563
2EF42299CA3BB5AF289540DA /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Intents.strings; sourceTree = "<group>"; };
561564
3FFDCEC6A6895EC4F495A160 /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = "<group>"; };
565+
46F99A7913E9584535D9F5FC /* TrustedHubHostsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedHubHostsViewModel.swift; sourceTree = "<group>"; };
562566
4A03255D25A368BF00E63D7A /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = "<group>"; };
563567
4A03257725A36A6900E63D7A /* VaultListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewController.swift; sourceTree = "<group>"; };
564568
4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Preview.swift"; sourceTree = "<group>"; };
@@ -2058,10 +2062,20 @@
20582062
740D367D266A18DF0058744D /* SettingsViewController.swift */,
20592063
740D3681266A19150058744D /* SettingsViewModel.swift */,
20602064
7408E6CB26779BC200D7FAEA /* About */,
2065+
8DB472C9A9C7437C46DCAFD7 /* Hub */,
20612066
);
20622067
path = Settings;
20632068
sourceTree = "<group>";
20642069
};
2070+
8DB472C9A9C7437C46DCAFD7 /* Hub */ = {
2071+
isa = PBXGroup;
2072+
children = (
2073+
0964E7C048AC1D59105CF75C /* TrustedHubHostsViewController.swift */,
2074+
46F99A7913E9584535D9F5FC /* TrustedHubHostsViewModel.swift */,
2075+
);
2076+
path = Hub;
2077+
sourceTree = "<group>";
2078+
};
20652079
743D95FA2D76EE88002D73C3 /* MicrosoftGraph */ = {
20662080
isa = PBXGroup;
20672081
children = (
@@ -2973,6 +2987,8 @@
29732987
74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */,
29742988
74F5DC1C26DCD2FB00AFE989 /* StoreObserver.swift in Sources */,
29752989
4A21B49626BC0270000D13DF /* VaultDetailInfoFooterViewModel.swift in Sources */,
2990+
11E84AE6F7F105AB9A0E2EAE /* TrustedHubHostsViewController.swift in Sources */,
2991+
6436F6BD08894DAD03812751 /* TrustedHubHostsViewModel.swift in Sources */,
29762992
);
29772993
runOnlyForDeploymentPostprocessing = 0;
29782994
};
@@ -3422,7 +3438,7 @@
34223438
GCC_WARN_UNUSED_FUNCTION = YES;
34233439
GCC_WARN_UNUSED_VARIABLE = YES;
34243440
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
3425-
MARKETING_VERSION = 2.8.2;
3441+
MARKETING_VERSION = 2.8.3;
34263442
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
34273443
MTL_FAST_MATH = YES;
34283444
ONLY_ACTIVE_ARCH = YES;
@@ -3484,7 +3500,7 @@
34843500
GCC_WARN_UNUSED_FUNCTION = YES;
34853501
GCC_WARN_UNUSED_VARIABLE = YES;
34863502
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
3487-
MARKETING_VERSION = 2.8.2;
3503+
MARKETING_VERSION = 2.8.3;
34883504
MTL_ENABLE_DEBUG_INFO = NO;
34893505
MTL_FAST_MATH = YES;
34903506
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200";
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//
2+
// TrustedHubHostsViewController.swift
3+
// Cryptomator
4+
//
5+
// Created by Tobias Hagemann on 12.03.26.
6+
// Copyright © 2026 Skymatic GmbH. All rights reserved.
7+
//
8+
9+
import CryptomatorCommonCore
10+
import UIKit
11+
12+
class TrustedHubHostsViewController: BaseUITableViewController {
13+
private enum Section {
14+
case hosts
15+
case clearAll
16+
}
17+
18+
private enum Item: Hashable {
19+
case host(String)
20+
case clearAll
21+
}
22+
23+
private class DataSource: UITableViewDiffableDataSource<Section, Item> {
24+
var deleteRowAction: ((UITableView, UITableViewCell.EditingStyle, IndexPath) -> Void)?
25+
26+
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
27+
guard let item = itemIdentifier(for: indexPath) else { return false }
28+
switch item {
29+
case .host:
30+
return true
31+
case .clearAll:
32+
return false
33+
}
34+
}
35+
36+
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
37+
deleteRowAction?(tableView, editingStyle, indexPath)
38+
}
39+
}
40+
41+
private let viewModel: TrustedHubHostsViewModel
42+
private var dataSource: DataSource?
43+
private lazy var emptyListMessage: UIView = {
44+
let label = UILabel()
45+
label.font = .preferredFont(forTextStyle: .body)
46+
label.adjustsFontForContentSizeCategory = true
47+
label.textAlignment = .center
48+
label.numberOfLines = 0
49+
label.text = viewModel.emptyListMessage
50+
label.translatesAutoresizingMaskIntoConstraints = false
51+
52+
let container = UIView()
53+
container.addSubview(label)
54+
NSLayoutConstraint.activate([
55+
label.leadingAnchor.constraint(equalTo: container.readableContentGuide.leadingAnchor),
56+
label.trailingAnchor.constraint(equalTo: container.readableContentGuide.trailingAnchor),
57+
label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
58+
])
59+
return container
60+
}()
61+
62+
init(viewModel: TrustedHubHostsViewModel) {
63+
self.viewModel = viewModel
64+
super.init()
65+
}
66+
67+
override func viewDidLoad() {
68+
super.viewDidLoad()
69+
title = viewModel.title
70+
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "HostCell")
71+
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ClearAllCell")
72+
configureDataSource()
73+
applySnapshot()
74+
}
75+
76+
private func configureDataSource() {
77+
dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in
78+
switch item {
79+
case let .host(host):
80+
let cell = tableView.dequeueReusableCell(withIdentifier: "HostCell", for: indexPath)
81+
cell.textLabel?.text = host
82+
cell.selectionStyle = .none
83+
return cell
84+
case .clearAll:
85+
let cell = tableView.dequeueReusableCell(withIdentifier: "ClearAllCell", for: indexPath)
86+
cell.textLabel?.text = LocalizedString.getValue("trustedHubHosts.clearAll")
87+
cell.textLabel?.textColor = .systemRed
88+
cell.selectionStyle = .default
89+
return cell
90+
}
91+
}
92+
dataSource?.deleteRowAction = { [weak self] _, editingStyle, indexPath in
93+
guard editingStyle == .delete,
94+
let self,
95+
let item = self.dataSource?.itemIdentifier(for: indexPath),
96+
case let .host(host) = item else { return }
97+
self.deleteHost(host)
98+
}
99+
}
100+
101+
private func applySnapshot() {
102+
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
103+
let hosts = viewModel.hosts
104+
if hosts.isEmpty {
105+
if isEditing {
106+
setEditing(false, animated: false)
107+
}
108+
navigationItem.rightBarButtonItem = nil
109+
tableView.backgroundView = emptyListMessage
110+
tableView.contentInsetAdjustmentBehavior = .never
111+
tableView.separatorStyle = .none
112+
} else {
113+
navigationItem.rightBarButtonItem = editButtonItem
114+
tableView.backgroundView = nil
115+
tableView.contentInsetAdjustmentBehavior = .automatic
116+
tableView.separatorStyle = .singleLine
117+
snapshot.appendSections([.hosts, .clearAll])
118+
snapshot.appendItems(hosts.map { .host($0) }, toSection: .hosts)
119+
snapshot.appendItems([.clearAll], toSection: .clearAll)
120+
}
121+
dataSource?.apply(snapshot, animatingDifferences: false)
122+
}
123+
124+
// MARK: - UITableViewDelegate
125+
126+
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
127+
tableView.deselectRow(at: indexPath, animated: true)
128+
guard let item = dataSource?.itemIdentifier(for: indexPath) else { return }
129+
switch item {
130+
case .host:
131+
break
132+
case .clearAll:
133+
showClearAllAlert()
134+
}
135+
}
136+
137+
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
138+
guard let item = dataSource?.itemIdentifier(for: indexPath), case let .host(host) = item else {
139+
return nil
140+
}
141+
let deleteAction = UIContextualAction(style: .destructive, title: LocalizedString.getValue("common.button.remove")) { [weak self] _, _, completion in
142+
self?.deleteHost(host)
143+
completion(true)
144+
}
145+
return UISwipeActionsConfiguration(actions: [deleteAction])
146+
}
147+
148+
// MARK: - Private
149+
150+
private func deleteHost(_ host: String) {
151+
viewModel.removeHost(host)
152+
applySnapshot()
153+
}
154+
155+
private func showClearAllAlert() {
156+
let alertController = UIAlertController(title: nil, message: LocalizedString.getValue("trustedHubHosts.clearAll.alert.message"), preferredStyle: .alert)
157+
let clearAction = UIAlertAction(title: LocalizedString.getValue("common.button.clear"), style: .destructive) { [weak self] _ in
158+
self?.viewModel.clearAllHosts()
159+
self?.applySnapshot()
160+
}
161+
let cancelAction = UIAlertAction(title: LocalizedString.getValue("common.button.cancel"), style: .cancel)
162+
alertController.addAction(clearAction)
163+
alertController.addAction(cancelAction)
164+
present(alertController, animated: true, completion: nil)
165+
}
166+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// TrustedHubHostsViewModel.swift
3+
// Cryptomator
4+
//
5+
// Created by Tobias Hagemann on 12.03.26.
6+
// Copyright © 2026 Skymatic GmbH. All rights reserved.
7+
//
8+
9+
import CryptomatorCommonCore
10+
import Foundation
11+
12+
class TrustedHubHostsViewModel {
13+
var title: String {
14+
return LocalizedString.getValue("settings.hub.trustedHosts")
15+
}
16+
17+
var emptyListMessage: String {
18+
return LocalizedString.getValue("trustedHubHosts.emptyList.message")
19+
}
20+
21+
var hosts: [String] {
22+
return cryptomatorSettings.trustedHubAuthorities.sorted()
23+
}
24+
25+
private var cryptomatorSettings: CryptomatorSettings
26+
27+
init(cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared) {
28+
self.cryptomatorSettings = cryptomatorSettings
29+
}
30+
31+
func removeHost(_ host: String) {
32+
var authorities = cryptomatorSettings.trustedHubAuthorities
33+
authorities.remove(host)
34+
cryptomatorSettings.trustedHubAuthorities = authorities
35+
}
36+
37+
func clearAllHosts() {
38+
cryptomatorSettings.trustedHubAuthorities = []
39+
}
40+
}

Cryptomator/Settings/SettingsCoordinator.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class SettingsCoordinator: Coordinator {
4949
navigationController.present(activityController, animated: true)
5050
}
5151

52+
func showTrustedHubHosts() {
53+
let viewModel = TrustedHubHostsViewModel()
54+
let viewController = TrustedHubHostsViewController(viewModel: viewModel)
55+
navigationController.pushViewController(viewController, animated: true)
56+
}
57+
5258
func showCloudServices() {
5359
let viewModel = ChooseCloudViewModel(clouds: [.dropbox, .googleDrive, .microsoftGraph(type: .oneDrive), .microsoftGraph(type: .sharePoint), .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom)], headerTitle: "")
5460
let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel)

Cryptomator/Settings/SettingsViewController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ class SettingsViewController: StaticUITableViewController<SettingsSection> {
115115
sendLogFile(sender: cell)
116116
case .clearCache:
117117
clearCache()
118+
case .showTrustedHubHosts:
119+
coordinator?.showTrustedHubHosts()
118120
case .showCloudServices:
119121
showCloudServices()
120122
case .showContact:

Cryptomator/Settings/SettingsViewModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ enum SettingsButtonAction: String {
1818
case showAbout
1919
case sendLogFile
2020
case clearCache
21+
case showTrustedHubHosts
2122
case showCloudServices
2223
case showContact
2324
case showRateApp
@@ -32,6 +33,7 @@ enum SettingsSection: Int {
3233
case unlockFullVersionSection = 0
3334
case cloudServiceSection
3435
case cacheSection
36+
case hubSection
3537
case aboutSection
3638
case debugSection
3739
case miscSection
@@ -72,6 +74,9 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
7274
cacheSizeCellViewModel,
7375
clearCacheButtonCellViewModel
7476
]),
77+
Section(id: .hubSection, elements: [
78+
ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showTrustedHubHosts, title: LocalizedString.getValue("settings.hub.trustedHosts"))
79+
]),
7580
Section(id: .aboutSection, elements: aboutSectionElements),
7681
Section(id: .debugSection, elements: [
7782
debugModeViewModel,

CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public protocol CryptomatorSettings {
1515
var trialExpirationDate: Date? { get set }
1616
var fullVersionUnlocked: Bool { get set }
1717
var hasRunningSubscription: Bool { get set }
18+
var trustedHubAuthorities: Set<String> { get set }
1819
#if !ALWAYS_PREMIUM
1920
var tenthAnniversaryBannerDismissed: Bool {
2021
get set
@@ -118,6 +119,11 @@ extension CryptomatorUserDefaults: CryptomatorSettings {
118119
set { write(value: newValue) }
119120
}
120121

122+
public var trustedHubAuthorities: Set<String> {
123+
get { Set(read() as [String]? ?? []) }
124+
set { write(value: Array(newValue).sorted()) }
125+
}
126+
121127
#if !ALWAYS_PREMIUM
122128
public var tenthAnniversaryBannerDismissed: Bool {
123129
get { read() ?? false }

0 commit comments

Comments
 (0)