Skip to content

Commit 239b840

Browse files
Auto upload via Albums: Improvements (#3360)
* Revert "rollback (#3310)" This reverts commit 9554b4e. * Add photos timeframe Signed-off-by: Milen Pivchev <[email protected]> * Strings Signed-off-by: Milen Pivchev <[email protected]> * Auto upload changes - Select timeframe - Remove unneeded toggle - Fix duplicate photos Signed-off-by: Milen Pivchev <[email protected]> * Small fixes Signed-off-by: Milen Pivchev <[email protected]> * Handle permissoins Signed-off-by: Milen Pivchev <[email protected]> * Fix thumbnail not showing for shared albums Signed-off-by: Milen Pivchev <[email protected]> * Fix albums not populating on permission granted Signed-off-by: Milen Pivchev <[email protected]> * Refactor Signed-off-by: Milen Pivchev <[email protected]> * PR fixes Signed-off-by: Milen Pivchev <[email protected]> * Padding, asset count Signed-off-by: Milen Pivchev <[email protected]> * Experiments Signed-off-by: Milen Pivchev <[email protected]> * PR fixes Signed-off-by: Milen Pivchev <[email protected]> * Add some UI tests Signed-off-by: Milen Pivchev <[email protected]> * cleaning old code Signed-off-by: Marino Faggiana <[email protected]> * cleaning code Signed-off-by: Marino Faggiana <[email protected]> * Fix Signed-off-by: Milen Pivchev <[email protected]> * WIP Signed-off-by: Milen Pivchev <[email protected]> * Refactor Signed-off-by: Milen Pivchev <[email protected]> * Refactor Signed-off-by: Milen Pivchev <[email protected]> * WIP Signed-off-by: Milen Pivchev <[email protected]> * fix create directory Signed-off-by: Marino Faggiana <[email protected]> * create folder improvements Signed-off-by: Marino Faggiana <[email protected]> * cleaning Signed-off-by: Marino Faggiana <[email protected]> * cleaning Signed-off-by: Marino Faggiana <[email protected]> * WIP Signed-off-by: Milen Pivchev <[email protected]> * WIP Signed-off-by: Milen Pivchev <[email protected]> * code improvements Signed-off-by: Marino Faggiana <[email protected]> * WIP Signed-off-by: Milen Pivchev <[email protected]> * code improvements Signed-off-by: Marino Faggiana <[email protected]> --------- Signed-off-by: Milen Pivchev <[email protected]> Signed-off-by: Marino Faggiana <[email protected]> Co-authored-by: Marino Faggiana <[email protected]>
1 parent d590f07 commit 239b840

28 files changed

+1088
-366
lines changed

Brand/Database.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ import Foundation
2727
//
2828
let databaseName = "nextcloud.realm"
2929
let tableAccountBackup = "tableAccountBackup.json"
30-
let databaseSchemaVersion: UInt64 = 379
30+
let databaseSchemaVersion: UInt64 = 380

Nextcloud.xcodeproj/project.pbxproj

Lines changed: 77 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// SPDX-FileCopyrightText: Nextcloud GmbH
2+
// SPDX-FileCopyrightText: 2025 Iva Horn
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
import XCTest
6+
7+
///
8+
/// User interface tests for the download limits management on shares.
9+
///
10+
@MainActor
11+
final class AutoUploadUITests: BaseUIXCTestCase {
12+
// MARK: - Lifecycle
13+
override func setUp() async throws {
14+
try await super.setUp()
15+
continueAfterFailure = false
16+
17+
// Handle alerts presented by the system.
18+
addUIInterruptionMonitor(withDescription: "Allow Notifications", for: "Allow")
19+
addUIInterruptionMonitor(withDescription: "Save Password", for: "Not Now")
20+
21+
// Launch the app.
22+
app = XCUIApplication()
23+
app.launchArguments = ["UI_TESTING"]
24+
app.launch()
25+
26+
try await logIn()
27+
28+
// Set up test backend communication.
29+
backend = UITestBackend()
30+
}
31+
32+
private func goToAutoUpload() async throws {
33+
addUIInterruptionMonitor(withDescription: "Are you sure you want to upload all photos?", for: "Confirm")
34+
35+
app.tabBars["Tab Bar"].buttons["More"].tap()
36+
37+
app.tables/*@START_MENU_TOKEN@*/.staticTexts["Settings"]/*[[".cells.staticTexts[\"Settings\"]",".staticTexts[\"Settings\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
38+
39+
let collectionViewsQuery = app.collectionViews
40+
collectionViewsQuery/*@START_MENU_TOKEN@*/.staticTexts["Auto upload"]/*[[".cells",".buttons[\"Auto upload\"].staticTexts[\"Auto upload\"]",".staticTexts[\"Auto upload\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.tap()
41+
42+
try await aSmallMoment()
43+
44+
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
45+
let allowButton = springboard.buttons["Allow Full Access"]
46+
47+
if allowButton.await() {
48+
allowButton.tap()
49+
}
50+
}
51+
52+
func testAutoUploadAllPhotos() async throws {
53+
try await goToAutoUpload()
54+
55+
let collectionViewsQuery = app.collectionViews
56+
57+
let turnOnAutoUploadingSwitch = collectionViewsQuery.switches["Turn on auto uploading"]
58+
59+
collectionViewsQuery/*@START_MENU_TOKEN@*/.switches["Auto upload photos"]/*[[".cells.switches[\"Auto upload photos\"]",".switches[\"Auto upload photos\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.swipeUp()
60+
61+
if turnOnAutoUploadingSwitch.await() {
62+
turnOnAutoUploadingSwitch.tap()
63+
}
64+
65+
app.tabBars["Tab Bar"].buttons["Files"].tap()
66+
67+
try await aSmallMoment()
68+
69+
pullToRefresh()
70+
71+
let photosItem = app.collectionViews["NCCollectionViewCommon"]/*@START_MENU_TOKEN@*/.staticTexts["Photos"]/*[[".cells[\"Photos\"].staticTexts[\"Photos\"]",".cells[\"Cell\/Photos\"].staticTexts[\"Photos\"]",".staticTexts[\"Photos\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/
72+
if photosItem.await() {
73+
photosItem.tap()
74+
}
75+
76+
try await aSmallMoment()
77+
78+
XCTAssertTrue(app.collectionViews.cells.count == 6)
79+
}
80+
81+
func testAutoUploadNewPhotos() async throws {
82+
try await goToAutoUpload()
83+
84+
let backUpNewPhotosVideosOnlySwitch = app.collectionViews.switches["NewPhotosToggle"]
85+
86+
if backUpNewPhotosVideosOnlySwitch.await() {
87+
backUpNewPhotosVideosOnlySwitch.switches.firstMatch.tap()
88+
}
89+
90+
let collectionViewsQuery = app.collectionViews
91+
92+
let turnOnAutoUploadingSwitch = collectionViewsQuery.switches["Turn on auto uploading"]
93+
94+
collectionViewsQuery/*@START_MENU_TOKEN@*/.switches["Auto upload photos"]/*[[".cells.switches[\"Auto upload photos\"]",".switches[\"Auto upload photos\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.swipeUp()
95+
96+
if turnOnAutoUploadingSwitch.await() {
97+
turnOnAutoUploadingSwitch.tap()
98+
}
99+
100+
app.tabBars["Tab Bar"].buttons["Files"].tap()
101+
102+
try await aSmallMoment()
103+
104+
pullToRefresh()
105+
106+
let photosItem = app.collectionViews["NCCollectionViewCommon"]/*@START_MENU_TOKEN@*/.staticTexts["Photos"]/*[[".cells[\"Photos\"].staticTexts[\"Photos\"]",".cells[\"Cell\/Photos\"].staticTexts[\"Photos\"]",".staticTexts[\"Photos\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/
107+
108+
// Does not seem possible to take a screenshot on Simulator or easily transfer a new photo in Simulator.
109+
// Thus for now we can only rely on the Photos folder not existing at all to test this.
110+
XCTAssertFalse(photosItem.exists)
111+
}
112+
113+
override func tearDown() async throws {
114+
let tabBar = app.tabBars["Tab Bar"]
115+
tabBar.buttons["Files"].tap()
116+
let nccollectionviewcommonCollectionView = app.collectionViews["NCCollectionViewCommon"]
117+
let cell = nccollectionviewcommonCollectionView/*@START_MENU_TOKEN@*/.cells["Cell/Photos"]/*[[".cells[\"Photos\"]",".cells[\"Cell\/Photos\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/
118+
if !cell.exists { return }
119+
120+
cell.otherElements.containing(.button, identifier:"Cell/Photos/shareButton").children(matching: .button).element(boundBy: 1).tap()
121+
let tablesQuery = app.tables
122+
tablesQuery/*@START_MENU_TOKEN@*/.staticTexts["Delete folder"]/*[[".cells.staticTexts[\"Delete folder\"]",".staticTexts[\"Delete folder\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
123+
app.alerts["Delete folder?"].scrollViews.otherElements.buttons["Yes"].tap()
124+
125+
cell.awaitInexistence()
126+
}
127+
}

Tests/NextcloudUITests/BaseUIXCTestCase.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,34 +72,61 @@ class BaseUIXCTestCase: XCTestCase {
7272
let serverAddressTextField = app.textFields["serverAddress"].firstMatch
7373
guard serverAddressTextField.await() else { return }
7474

75+
try await aSmallMoment()
76+
7577
serverAddressTextField.tap()
7678
serverAddressTextField.typeText(TestConstants.server)
7779

7880
app.buttons["submitServerAddress"].tap()
7981

82+
try await aSmallMoment()
83+
8084
let webView = app.webViews.firstMatch
8185

8286
guard webView.await() else {
8387
throw UITestError.waitForExistence(webView)
8488
}
8589

90+
// try await aSmallMoment()
91+
8692
let loginButton = webView.buttons["Log in"]
8793

94+
// try await aSmallMoment()
95+
8896
if loginButton.await() {
8997
loginButton.tap()
9098
}
9199

100+
// try await aSmallMoment()
101+
92102
let usernameTextField = webView.textFields.firstMatch
93103

94104
if usernameTextField.await() {
105+
106+
try await aSmallMoment()
107+
95108
guard usernameTextField.await() else { return }
96109
usernameTextField.tap()
110+
111+
try await aSmallMoment()
112+
97113
usernameTextField.typeText(TestConstants.username)
98114

115+
try await aSmallMoment()
116+
99117
let passwordSecureTextField = webView.secureTextFields.firstMatch
118+
119+
try await aSmallMoment()
120+
100121
passwordSecureTextField.tap()
122+
123+
124+
try await aSmallMoment()
125+
101126
passwordSecureTextField.typeText(TestConstants.password)
102127

128+
try await aSmallMoment()
129+
103130
webView.buttons.firstMatch.tap()
104131
}
105132

iOSClient/Data/NCManageDatabase+Account.swift

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ class tableAccount: Object {
3131
@objc dynamic var active: Bool = false
3232
@objc dynamic var address = ""
3333
@objc dynamic var alias = ""
34-
@objc dynamic var autoUpload: Bool = false
3534
@objc dynamic var autoUploadCreateSubfolder: Bool = false
3635
@objc dynamic var autoUploadSubfolderGranularity: Int = NCGlobal.shared.subfolderGranularityMonthly
3736
@objc dynamic var autoUploadDirectory = ""
3837
@objc dynamic var autoUploadFileName = ""
39-
@objc dynamic var autoUploadFull: Bool = false
38+
@objc dynamic var autoUploadStart: Bool = false
4039
@objc dynamic var autoUploadImage: Bool = false
4140
@objc dynamic var autoUploadVideo: Bool = false
42-
@objc dynamic var autoUploadFavoritesOnly: Bool = false
4341
@objc dynamic var autoUploadWWAnPhoto: Bool = false
4442
@objc dynamic var autoUploadWWAnVideo: Bool = false
43+
/// The Date from which new photos should be uploaded
44+
@objc dynamic var autoUploadSinceDate: Date?
45+
/// The date of the most recently uploaded asset
46+
@objc dynamic var autoUploadLastUploadedDate: Date?
4547
@objc dynamic var backend = ""
4648
@objc dynamic var backendCapabilitiesSetDisplayName: Bool = false
4749
@objc dynamic var backendCapabilitiesSetPassword: Bool = false
@@ -80,7 +82,7 @@ class tableAccount: Object {
8082
}
8183

8284
func tableAccountToCodable() -> tableAccountCodable {
83-
return tableAccountCodable(account: self.account, active: self.active, alias: self.alias, autoUpload: self.autoUpload, autoUploadCreateSubfolder: self.autoUploadCreateSubfolder, autoUploadSubfolderGranularity: self.autoUploadSubfolderGranularity, autoUploadDirectory: self.autoUploadDirectory, autoUploadFileName: self.autoUploadFileName, autoUploadFull: self.autoUploadFull, autoUploadImage: self.autoUploadImage, autoUploadVideo: self.autoUploadVideo, autoUploadFavoritesOnly: self.autoUploadFavoritesOnly, autoUploadWWAnPhoto: self.autoUploadWWAnPhoto, autoUploadWWAnVideo: self.autoUploadWWAnVideo, user: self.user, userId: self.userId, urlBase: self.urlBase)
85+
return tableAccountCodable(account: self.account, active: self.active, alias: self.alias, autoUploadCreateSubfolder: self.autoUploadCreateSubfolder, autoUploadSubfolderGranularity: self.autoUploadSubfolderGranularity, autoUploadDirectory: self.autoUploadDirectory, autoUploadFileName: self.autoUploadFileName, autoUploadStart: self.autoUploadStart, autoUploadImage: self.autoUploadImage, autoUploadVideo: self.autoUploadVideo, autoUploadWWAnPhoto: self.autoUploadWWAnPhoto, autoUploadWWAnVideo: self.autoUploadWWAnVideo, user: self.user, userId: self.userId, urlBase: self.urlBase)
8486
}
8587

8688
convenience init(codableObject: tableAccountCodable) {
@@ -89,15 +91,13 @@ class tableAccount: Object {
8991
self.active = codableObject.active
9092
self.alias = codableObject.alias
9193

92-
self.autoUpload = codableObject.autoUpload
9394
self.autoUploadCreateSubfolder = codableObject.autoUploadCreateSubfolder
9495
self.autoUploadSubfolderGranularity = codableObject.autoUploadSubfolderGranularity
9596
self.autoUploadDirectory = codableObject.autoUploadDirectory
9697
self.autoUploadFileName = codableObject.autoUploadFileName
97-
self.autoUploadFull = codableObject.autoUploadFull
98+
self.autoUploadStart = codableObject.autoUploadStart
9899
self.autoUploadImage = codableObject.autoUploadImage
99100
self.autoUploadVideo = codableObject.autoUploadVideo
100-
self.autoUploadFavoritesOnly = codableObject.autoUploadFavoritesOnly
101101
self.autoUploadWWAnPhoto = codableObject.autoUploadWWAnPhoto
102102
self.autoUploadWWAnVideo = codableObject.autoUploadWWAnVideo
103103

@@ -112,15 +112,13 @@ struct tableAccountCodable: Codable {
112112
var active: Bool
113113
var alias: String
114114

115-
var autoUpload: Bool
116115
var autoUploadCreateSubfolder: Bool
117116
var autoUploadSubfolderGranularity: Int
118117
var autoUploadDirectory = ""
119118
var autoUploadFileName: String
120-
var autoUploadFull: Bool
119+
var autoUploadStart: Bool
121120
var autoUploadImage: Bool
122121
var autoUploadVideo: Bool
123-
var autoUploadFavoritesOnly: Bool
124122
var autoUploadWWAnPhoto: Bool
125123
var autoUploadWWAnVideo: Bool
126124

@@ -213,6 +211,12 @@ extension NCManageDatabase {
213211
}
214212
}
215213

214+
func updateAccountProperty<T>(_ keyPath: ReferenceWritableKeyPath<tableAccount, T>, value: T, account: String) {
215+
guard let activeAccount = getTableAccount(account: account) else { return }
216+
activeAccount[keyPath: keyPath] = value
217+
updateAccount(activeAccount)
218+
}
219+
216220
func updateAccount(_ account: tableAccount) {
217221
do {
218222
let realm = try Realm()
@@ -347,6 +351,18 @@ extension NCManageDatabase {
347351
return NCGlobal.shared.subfolderGranularityMonthly
348352
}
349353

354+
func getAccountAutoUploadFromFromDate() -> Date? {
355+
do {
356+
let realm = try Realm()
357+
guard let result = realm.objects(tableAccount.self).filter("active == true").first else { return .distantPast }
358+
return result.autoUploadSinceDate
359+
} catch let error as NSError {
360+
NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)")
361+
}
362+
363+
return nil
364+
}
365+
350366
@discardableResult
351367
func setAccountActive(_ account: String) -> tableAccount? {
352368
var tblAccount: tableAccount?

iOSClient/Data/NCManageDatabase.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ final class NCManageDatabase: Sendable {
7272
tableTag.self,
7373
tableAccount.self,
7474
tableCapabilities.self,
75-
tablePhotoLibrary.self,
7675
tableE2eEncryption.self,
7776
tableE2eEncryptionLock.self,
7877
tableE2eMetadata12.self,
@@ -220,7 +219,6 @@ final class NCManageDatabase: Sendable {
220219
self.clearTable(TableGroupfoldersGroups.self, account: account)
221220
self.clearTable(tableLocalFile.self, account: account)
222221
self.clearTable(tableMetadata.self, account: account)
223-
self.clearTable(tablePhotoLibrary.self, account: account)
224222
self.clearTable(tableShare.self, account: account)
225223
self.clearTable(TableSecurityGuardDiagnostics.self, account: account)
226224
self.clearTable(tableTag.self, account: account)

iOSClient/DeepLink/NCDeepLinkHandler.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@ class NCDeepLinkHandler {
150150
controller.selectedIndex = ControllerConstants.moreIndex
151151
guard let navigationController = controller.viewControllers?[controller.selectedIndex] as? UINavigationController else { return }
152152

153-
let autoUploadView = NCAutoUploadView(model: NCAutoUploadModel(controller: controller))
154-
let autoUploadController = UIHostingController(rootView: autoUploadView)
155-
navigationController.pushViewController(autoUploadController, animated: true)
153+
Task { @MainActor in
154+
let autoUploadView = NCAutoUploadView(model: NCAutoUploadModel(controller: controller), albumModel: AlbumModel(controller: controller))
155+
let autoUploadController = UIHostingController(rootView: autoUploadView)
156+
navigationController.pushViewController(autoUploadController, animated: true)
157+
}
156158
}
157159

158160
private func navigateAppUpdate() {

iOSClient/Extensions/PHAssetCollection+Extension.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import Photos
66

77
extension PHAssetCollection {
8+
public static func == (lhs: PHAssetCollection, rhs: PHAssetCollection) -> Bool {
9+
return lhs.localIdentifier == rhs.localIdentifier || lhs.assetCount == rhs.assetCount
10+
}
11+
812
var assetCount: Int {
913
let fetchOptions = PHFetchOptions()
1014
let result = PHAsset.fetchAssets(in: self, options: fetchOptions)

iOSClient/Extensions/UIAlertController+Extension.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,28 @@ extension UIAlertController {
6767
completion?(error)
6868
}
6969
#else
70-
let metadataForCreateFolder = NCManageDatabase.shared.createMetadata(fileName: fileNameFolder,
71-
fileNameView: fileNameFolder,
72-
ocId: NSUUID().uuidString,
73-
serverUrl: serverUrl,
74-
url: "",
75-
contentType: "httpd/unix-directory",
76-
directory: true,
77-
session: session,
78-
sceneIdentifier: sceneIdentifier)
79-
metadataForCreateFolder.status = NCGlobal.shared.metadataStatusWaitCreateFolder
80-
metadataForCreateFolder.sessionDate = Date()
81-
NCManageDatabase.shared.addMetadata(metadataForCreateFolder)
82-
NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateFolder, userInfo: ["ocId": metadataForCreateFolder.ocId, "serverUrl": metadataForCreateFolder.serverUrl, "account": metadataForCreateFolder.account, "withPush": true, "sceneIdentifier": sceneIdentifier as Any])
70+
var metadata = tableMetadata()
71+
72+
if let result = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameView == %@", session.account, serverUrl, fileNameFolder)) {
73+
metadata = result
74+
} else {
75+
metadata = NCManageDatabase.shared.createMetadata(fileName: fileNameFolder,
76+
fileNameView: fileNameFolder,
77+
ocId: NSUUID().uuidString,
78+
serverUrl: serverUrl,
79+
url: "",
80+
contentType: "httpd/unix-directory",
81+
directory: true,
82+
session: session,
83+
sceneIdentifier: sceneIdentifier)
84+
}
85+
86+
metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder
87+
metadata.sessionDate = Date()
88+
89+
NCManageDatabase.shared.addMetadata(metadata)
90+
91+
NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateFolder, userInfo: ["ocId": metadata.ocId, "serverUrl": metadata.serverUrl, "account": metadata.account, "withPush": true, "sceneIdentifier": sceneIdentifier as Any])
8392
#endif
8493
}
8594
})

0 commit comments

Comments
 (0)