From 426bb05191b1b217f6b3ee72365f1c05fcea8498 Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Sat, 18 Apr 2026 11:52:45 +0300 Subject: [PATCH 1/5] Fix PerformDeletionsDbRequest for My Library case --- .../Requests/PerformDeletionsDbRequest.swift | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift b/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift index 2dba2a3bc..8ae2634aa 100644 --- a/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift @@ -149,16 +149,36 @@ struct PerformLastReadDeletionsDbRequest: DbRequest { var needsWrite: Bool { return true } func process(in database: Realm) throws { - let objects = database.objects(RLastReadDate.self).filter(.keys(keys, in: libraryId)) - for object in objects { - guard !object.isInvalidated else { continue } - if object.isChanged { - // If remotely deleted lastRead is changed locally, we want to keep the lastRead, so we mark that - // this lastRead is new and it will be reinserted by sync - object.markAsChanged(in: database) - } else { - object.willRemove(in: database) - database.delete(object) + switch libraryId { + case .custom(.myLibrary): + let objects = database.objects(RItem.self).filter(.keys(keys, in: libraryId)) + for object in objects { + guard !object.isInvalidated else { continue } + + // My Library stores last-read directly on the item, so only preserve it when the field itself + // has an unsynced local change. Other local item changes should not block a remote last-read clear. + if object.changedFields.contains(.lastRead) { + continue + } + + if object.lastRead != nil { + object.lastRead = nil + object.updateEffectiveLastRead() + } + } + + case .group: + let objects = database.objects(RLastReadDate.self).filter(.keys(keys, in: libraryId)) + for object in objects { + guard !object.isInvalidated else { continue } + if object.isChanged { + // If remotely deleted lastRead is changed locally, we want to keep the lastRead, so we mark that + // this lastRead is new and it will be reinserted by sync + object.markAsChanged(in: database) + } else { + object.willRemove(in: database) + database.delete(object) + } } } } From b66d6e46156901e553f1042a669123b936130abc Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Sat, 18 Apr 2026 11:53:36 +0300 Subject: [PATCH 2/5] Add updatable object tests for My Library last read deletions --- ZoteroTests/UpdatableObjectSpec.swift | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/ZoteroTests/UpdatableObjectSpec.swift b/ZoteroTests/UpdatableObjectSpec.swift index 98c788a18..0ae233d36 100644 --- a/ZoteroTests/UpdatableObjectSpec.swift +++ b/ZoteroTests/UpdatableObjectSpec.swift @@ -114,6 +114,61 @@ final class UpdatableObjectSpec: QuickSpec { expect(refinedBy["end"] as? Int).to(equal(1705)) } } + + context("when applying My Library last-read deletions") { + it("clears the item's last-read value") { + let key = "AAAAAAAA" + let date = Date(timeIntervalSince1970: 1234) + + try! realm.write { + let item = RItem() + item.key = key + item.rawType = ItemTypes.attachment + item.customLibraryKey = .myLibrary + item.dateAdded = Date() + item.dateModified = item.dateAdded + item.lastRead = date + item.updateEffectiveLastRead() + realm.add(item) + } + + try! realm.write { + try! PerformLastReadDeletionsDbRequest(libraryId: .custom(.myLibrary), keys: [key]).process(in: realm) + } + + let item = realm.objects(RItem.self).first! + expect(item.lastRead).to(beNil()) + expect(item.effectiveLastRead).to(beNil()) + } + + it("preserves unsynced local last-read changes") { + let key = "BBBBBBBB" + let date = Date(timeIntervalSince1970: 5678) + + try! realm.write { + let item = RItem() + item.key = key + item.rawType = ItemTypes.attachment + item.customLibraryKey = .myLibrary + item.dateAdded = Date() + item.dateModified = item.dateAdded + item.lastRead = date + item.updateEffectiveLastRead() + item.changes.append(RObjectChange.create(changes: RItemChanges.lastRead)) + item.changeType = .user + realm.add(item) + } + + try! realm.write { + try! PerformLastReadDeletionsDbRequest(libraryId: .custom(.myLibrary), keys: [key]).process(in: realm) + } + + let item = realm.objects(RItem.self).first! + expect(item.lastRead).to(equal(date)) + expect(item.effectiveLastRead).to(equal(date)) + expect(item.changedFields.contains(.lastRead)).to(beTrue()) + } + } } } } From 69abdfc5d1ffe843e7f4e9e3a44e67005c8d1121 Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Mon, 20 Apr 2026 14:37:17 +0300 Subject: [PATCH 3/5] Report last read deletions for My Library as non fatal sync errors --- Zotero/Assets/en.lproj/Localizable.strings | 1 + .../Requests/PerformDeletionsDbRequest.swift | 17 ++--------- .../PerformDeletionsSyncAction.swift | 19 +++++++++--- Zotero/Controllers/Sync/SyncController.swift | 16 ++++++---- Zotero/Models/Sync/SyncErrors.swift | 1 + .../General/Views/SyncToolbarController.swift | 3 ++ ZoteroTests/UpdatableObjectSpec.swift | 30 +------------------ 7 files changed, 34 insertions(+), 53 deletions(-) diff --git a/Zotero/Assets/en.lproj/Localizable.strings b/Zotero/Assets/en.lproj/Localizable.strings index 49dffaa8d..6f9348eee 100644 --- a/Zotero/Assets/en.lproj/Localizable.strings +++ b/Zotero/Assets/en.lproj/Localizable.strings @@ -418,6 +418,7 @@ "errors.sync_toolbar.conflict_retry_limit" = "Remote sync in progress. Please try again in a few minutes."; "errors.sync_toolbar.group_permissions" = "You don’t have permission to edit groups."; "errors.sync_toolbar.internet_connection" = "Unable to connect to the network. Please try again."; +"errors.sync_toolbar.unexpected_my_library_last_read_deletions" = "Sync received unexpected last read settings deletions for My Library."; "errors.sync_toolbar.personal_quota_reached" = "You have reached your Zotero Storage quota. Some files were not uploaded. Other Zotero data will continue to sync.\n\nSee your zotero.org account settings for additional storage options."; "errors.sync_toolbar.group_quota_reached" = "The group “%@” has reached its Zotero Storage quota. Some files were not uploaded. Other Zotero data will continue to sync.\n\nThe group owner can increase the group’s storage capacity from their storage settings on zotero.org."; "errors.sync_toolbar.insufficient_space" = "You have insufficient space on your WebDAV server. Some files were not uploaded. Other Zotero data will continue to sync."; diff --git a/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift b/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift index 8ae2634aa..ad64375c3 100644 --- a/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift @@ -8,6 +8,7 @@ import Foundation +import CocoaLumberjackSwift import RealmSwift struct PerformItemDeletionsDbRequest: DbResponseRequest { @@ -151,21 +152,7 @@ struct PerformLastReadDeletionsDbRequest: DbRequest { func process(in database: Realm) throws { switch libraryId { case .custom(.myLibrary): - let objects = database.objects(RItem.self).filter(.keys(keys, in: libraryId)) - for object in objects { - guard !object.isInvalidated else { continue } - - // My Library stores last-read directly on the item, so only preserve it when the field itself - // has an unsynced local change. Other local item changes should not block a remote last-read clear. - if object.changedFields.contains(.lastRead) { - continue - } - - if object.lastRead != nil { - object.lastRead = nil - object.updateEffectiveLastRead() - } - } + DDLogWarn("PerformLastReadDeletionsDbRequest: Ignoring deletion for My Library with keys \(keys)") case .group: let objects = database.objects(RLastReadDate.self).filter(.keys(keys, in: libraryId)) diff --git a/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift b/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift index 5a7cfca0e..98698ba6f 100644 --- a/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift +++ b/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift @@ -8,10 +8,11 @@ import Foundation +import CocoaLumberjackSwift import RxSwift struct PerformDeletionsSyncAction: SyncAction { - typealias Result = [(String, String)] + typealias Result = (conflicts: [(String, String)], unexpectedMyLibraryLastReadDeletions: [String]) private static let batchSize = 500 let libraryId: LibraryIdentifier @@ -24,7 +25,7 @@ struct PerformDeletionsSyncAction: SyncAction { unowned let dbStorage: DbStorage let queue: DispatchQueue - var result: Single<[(String, String)]> { + var result: Single { return Single.create { subscriber -> Disposable in do { let hasCollections = try dbStorage.perform(request: CountObjectsDbRequest(), on: queue) > 0 @@ -72,6 +73,7 @@ struct PerformDeletionsSyncAction: SyncAction { } } + var unexpectedMyLibraryLastReadDeletions: [String] = [] let lastRead = settings.filter({ $0.hasPrefix("lastRead") }) let hasLastRead = try dbStorage.perform(request: CountObjectsDbRequest(), on: queue) > 0 if hasLastRead { @@ -82,12 +84,21 @@ struct PerformDeletionsSyncAction: SyncAction { groupedIndices[libraryId, default: []].append(key) } for (libraryId, keys) in groupedIndices { - try dbStorage.perform(request: PerformLastReadDeletionsDbRequest(libraryId: libraryId, keys: keys), on: queue) + switch libraryId { + case .custom(.myLibrary): + unexpectedMyLibraryLastReadDeletions.append(contentsOf: keys) + + case .group: + try dbStorage.perform(request: PerformLastReadDeletionsDbRequest(libraryId: libraryId, keys: keys), on: queue) + } } } } + if !unexpectedMyLibraryLastReadDeletions.isEmpty { + DDLogWarn("PerformDeletionsSyncAction: Received unexpected My Library lastRead deletions - \(unexpectedMyLibraryLastReadDeletions)") + } - subscriber(.success(conflicts)) + subscriber(.success((conflicts: conflicts, unexpectedMyLibraryLastReadDeletions: unexpectedMyLibraryLastReadDeletions))) } catch let error { subscriber(.failure(error)) } diff --git a/Zotero/Controllers/Sync/SyncController.swift b/Zotero/Controllers/Sync/SyncController.swift index 04cb9a46d..0c3877124 100644 --- a/Zotero/Controllers/Sync/SyncController.swift +++ b/Zotero/Controllers/Sync/SyncController.swift @@ -470,7 +470,8 @@ final class SyncController: SynchronizationController { .webDavDeletionFailed, .webDavVerification, .webDavDownload, - .webDavUpload: + .webDavUpload, + .unexpectedMyLibraryLastReadDeletions: reportErrors.append(error) } } @@ -1394,9 +1395,9 @@ final class SyncController: SynchronizationController { queue: self.workQueue ) action.result.subscribe(on: self.workScheduler) - .subscribe(onSuccess: { [weak self] conflicts in + .subscribe(onSuccess: { [weak self] result in self?.accessQueue.async(flags: .barrier) { [weak self] in - self?.finishDeletionsSync(result: .success(conflicts), items: items, libraryId: libraryId) + self?.finishDeletionsSync(result: .success(result), items: items, libraryId: libraryId) } }, onFailure: { [weak self] error in self?.accessQueue.async(flags: .barrier) { [weak self] in @@ -1406,9 +1407,14 @@ final class SyncController: SynchronizationController { .disposed(by: self.disposeBag) } - private func finishDeletionsSync(result: Result<[(String, String)], Error>, items: [String]?, libraryId: LibraryIdentifier, version: Int? = nil) { + private func finishDeletionsSync(result: Result, items: [String]?, libraryId: LibraryIdentifier, version: Int? = nil) { switch result { - case .success(let conflicts): + case .success(let result): + if !result.unexpectedMyLibraryLastReadDeletions.isEmpty { + nonFatalErrors.append(.unexpectedMyLibraryLastReadDeletions(keys: result.unexpectedMyLibraryLastReadDeletions)) + } + + let conflicts = result.conflicts if !conflicts.isEmpty { self.resolve(conflict: .removedItemsHaveLocalChanges(keys: conflicts, libraryId: libraryId)) } else { diff --git a/Zotero/Models/Sync/SyncErrors.swift b/Zotero/Models/Sync/SyncErrors.swift index cc0c19a68..9830f1b35 100644 --- a/Zotero/Models/Sync/SyncErrors.swift +++ b/Zotero/Models/Sync/SyncErrors.swift @@ -79,6 +79,7 @@ enum SyncError { case webDavDownload(WebDavError.Download) case webDavUpload(WebDavError.Upload) case preconditionFailed(LibraryIdentifier) + case unexpectedMyLibraryLastReadDeletions(keys: [String]) var isVersionMismatch: Bool { switch self { diff --git a/Zotero/Scenes/General/Views/SyncToolbarController.swift b/Zotero/Scenes/General/Views/SyncToolbarController.swift index 842e69d62..c93495466 100644 --- a/Zotero/Scenes/General/Views/SyncToolbarController.swift +++ b/Zotero/Scenes/General/Views/SyncToolbarController.swift @@ -266,6 +266,9 @@ final class SyncToolbarController { case .preconditionFailed(let libraryId): return (L10n.Errors.SyncToolbar.conflictRetryLimit, SyncError.ErrorData(itemKeys: nil, libraryId: libraryId)) + + case .unexpectedMyLibraryLastReadDeletions(let keys): + return (L10n.Errors.SyncToolbar.unexpectedMyLibraryLastReadDeletions, SyncError.ErrorData(itemKeys: keys, libraryId: .custom(.myLibrary))) } } diff --git a/ZoteroTests/UpdatableObjectSpec.swift b/ZoteroTests/UpdatableObjectSpec.swift index 0ae233d36..76d999cfe 100644 --- a/ZoteroTests/UpdatableObjectSpec.swift +++ b/ZoteroTests/UpdatableObjectSpec.swift @@ -116,7 +116,7 @@ final class UpdatableObjectSpec: QuickSpec { } context("when applying My Library last-read deletions") { - it("clears the item's last-read value") { + it("ignores them and leaves the item's last-read value unchanged") { let key = "AAAAAAAA" let date = Date(timeIntervalSince1970: 1234) @@ -136,37 +136,9 @@ final class UpdatableObjectSpec: QuickSpec { try! PerformLastReadDeletionsDbRequest(libraryId: .custom(.myLibrary), keys: [key]).process(in: realm) } - let item = realm.objects(RItem.self).first! - expect(item.lastRead).to(beNil()) - expect(item.effectiveLastRead).to(beNil()) - } - - it("preserves unsynced local last-read changes") { - let key = "BBBBBBBB" - let date = Date(timeIntervalSince1970: 5678) - - try! realm.write { - let item = RItem() - item.key = key - item.rawType = ItemTypes.attachment - item.customLibraryKey = .myLibrary - item.dateAdded = Date() - item.dateModified = item.dateAdded - item.lastRead = date - item.updateEffectiveLastRead() - item.changes.append(RObjectChange.create(changes: RItemChanges.lastRead)) - item.changeType = .user - realm.add(item) - } - - try! realm.write { - try! PerformLastReadDeletionsDbRequest(libraryId: .custom(.myLibrary), keys: [key]).process(in: realm) - } - let item = realm.objects(RItem.self).first! expect(item.lastRead).to(equal(date)) expect(item.effectiveLastRead).to(equal(date)) - expect(item.changedFields.contains(.lastRead)).to(beTrue()) } } } From da2d5abd73150ba83f50e4a0762efe7e4ce3da2d Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Mon, 20 Apr 2026 23:27:32 +0300 Subject: [PATCH 4/5] Throw for PerformLastReadDeletionsDbRequest if passed My Library --- .../Requests/PerformDeletionsDbRequest.swift | 7 +++++-- .../SyncActions/PerformDeletionsSyncAction.swift | 14 +++++++++----- ZoteroTests/UpdatableObjectSpec.swift | 10 ++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift b/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift index ad64375c3..86edd5710 100644 --- a/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/PerformDeletionsDbRequest.swift @@ -8,7 +8,6 @@ import Foundation -import CocoaLumberjackSwift import RealmSwift struct PerformItemDeletionsDbRequest: DbResponseRequest { @@ -144,6 +143,10 @@ struct PerformPageIndexDeletionsDbRequest: DbRequest { } struct PerformLastReadDeletionsDbRequest: DbRequest { + enum Error: Swift.Error { + case myLibraryNotSupported + } + let libraryId: LibraryIdentifier let keys: [String] @@ -152,7 +155,7 @@ struct PerformLastReadDeletionsDbRequest: DbRequest { func process(in database: Realm) throws { switch libraryId { case .custom(.myLibrary): - DDLogWarn("PerformLastReadDeletionsDbRequest: Ignoring deletion for My Library with keys \(keys)") + throw Error.myLibraryNotSupported case .group: let objects = database.objects(RLastReadDate.self).filter(.keys(keys, in: libraryId)) diff --git a/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift b/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift index 98698ba6f..dcfc8d960 100644 --- a/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift +++ b/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift @@ -84,12 +84,16 @@ struct PerformDeletionsSyncAction: SyncAction { groupedIndices[libraryId, default: []].append(key) } for (libraryId, keys) in groupedIndices { - switch libraryId { - case .custom(.myLibrary): - unexpectedMyLibraryLastReadDeletions.append(contentsOf: keys) - - case .group: + do { try dbStorage.perform(request: PerformLastReadDeletionsDbRequest(libraryId: libraryId, keys: keys), on: queue) + } catch let error { + switch error { + case PerformLastReadDeletionsDbRequest.Error.myLibraryNotSupported: + unexpectedMyLibraryLastReadDeletions.append(contentsOf: keys) + + default: + throw error + } } } } diff --git a/ZoteroTests/UpdatableObjectSpec.swift b/ZoteroTests/UpdatableObjectSpec.swift index 76d999cfe..4b2cb7bf4 100644 --- a/ZoteroTests/UpdatableObjectSpec.swift +++ b/ZoteroTests/UpdatableObjectSpec.swift @@ -116,7 +116,7 @@ final class UpdatableObjectSpec: QuickSpec { } context("when applying My Library last-read deletions") { - it("ignores them and leaves the item's last-read value unchanged") { + it("throws and leaves the item's last-read value unchanged") { let key = "AAAAAAAA" let date = Date(timeIntervalSince1970: 1234) @@ -132,9 +132,11 @@ final class UpdatableObjectSpec: QuickSpec { realm.add(item) } - try! realm.write { - try! PerformLastReadDeletionsDbRequest(libraryId: .custom(.myLibrary), keys: [key]).process(in: realm) - } + expect { + try realm.write { + try PerformLastReadDeletionsDbRequest(libraryId: .custom(.myLibrary), keys: [key]).process(in: realm) + } + }.to(throwError()) let item = realm.objects(RItem.self).first! expect(item.lastRead).to(equal(date)) From 5c77bd1cca5626949f2204ceec8d11ea6104bc64 Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Mon, 20 Apr 2026 23:56:06 +0300 Subject: [PATCH 5/5] Fix lastRead and lastReadAloudPosition settings clash --- .../MarkObjectsAsSyncedDbRequest.swift | 4 +- .../PerformDeletionsSyncAction.swift | 4 +- .../SubmitDeletionSyncAction.swift | 2 +- ZoteroTests/SyncActionsSpec.swift | 42 +++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Zotero/Controllers/Database/Requests/MarkObjectsAsSyncedDbRequest.swift b/Zotero/Controllers/Database/Requests/MarkObjectsAsSyncedDbRequest.swift index 329a8eb56..611e5f223 100644 --- a/Zotero/Controllers/Database/Requests/MarkObjectsAsSyncedDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/MarkObjectsAsSyncedDbRequest.swift @@ -51,9 +51,9 @@ struct MarkSettingsAsSyncedDbRequest: DbRequest { func process(in database: Realm) throws { for setting in settings { let object: UpdatableObject&Syncable - if setting.uid.starts(with: "lastRead"), let lastRead = database.objects(RLastReadDate.self).uniqueObject(key: setting.key, libraryId: setting.libraryId) { + if setting.uid.starts(with: "lastRead_"), let lastRead = database.objects(RLastReadDate.self).uniqueObject(key: setting.key, libraryId: setting.libraryId) { object = lastRead - } else if setting.uid.starts(with: "lastPageIndex"), let pageIndex = database.objects(RPageIndex.self).uniqueObject(key: setting.key, libraryId: setting.libraryId) { + } else if setting.uid.starts(with: "lastPageIndex_"), let pageIndex = database.objects(RPageIndex.self).uniqueObject(key: setting.key, libraryId: setting.libraryId) { object = pageIndex } else { DDLogError("MarkSettingsAsSyncedDbRequest: could not find setting for \(setting.uid)") diff --git a/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift b/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift index dcfc8d960..032575ff2 100644 --- a/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift +++ b/Zotero/Controllers/Sync/SyncActions/PerformDeletionsSyncAction.swift @@ -58,7 +58,7 @@ struct PerformDeletionsSyncAction: SyncAction { } } - let pageIndices = settings.filter({ $0.hasPrefix("lastPageIndex") }) + let pageIndices = settings.filter({ $0.hasPrefix("lastPageIndex_") }) let hasPageIndices = try dbStorage.perform(request: CountObjectsDbRequest(), on: queue) > 0 if hasPageIndices { try batch(values: pageIndices, batchSize: Self.batchSize) { uids in @@ -74,7 +74,7 @@ struct PerformDeletionsSyncAction: SyncAction { } var unexpectedMyLibraryLastReadDeletions: [String] = [] - let lastRead = settings.filter({ $0.hasPrefix("lastRead") }) + let lastRead = settings.filter({ $0.hasPrefix("lastRead_") }) let hasLastRead = try dbStorage.perform(request: CountObjectsDbRequest(), on: queue) > 0 if hasLastRead { try batch(values: lastRead, batchSize: Self.batchSize) { uids in diff --git a/Zotero/Controllers/Sync/SyncActions/SubmitDeletionSyncAction.swift b/Zotero/Controllers/Sync/SyncActions/SubmitDeletionSyncAction.swift index f35a443ac..d5d4f6dc3 100644 --- a/Zotero/Controllers/Sync/SyncActions/SubmitDeletionSyncAction.swift +++ b/Zotero/Controllers/Sync/SyncActions/SubmitDeletionSyncAction.swift @@ -66,7 +66,7 @@ struct SubmitDeletionSyncAction: SyncAction { var groupedSettings: [LibraryIdentifier: [String]] = [:] for uid in keys { // only lastRead is deletable - guard uid.starts(with: "lastRead"), let (key, libraryId) = try? SettingKeyParser.parse(key: uid) else { continue } + guard uid.starts(with: "lastRead_"), let (key, libraryId) = try? SettingKeyParser.parse(key: uid) else { continue } groupedSettings[libraryId, default: []].append(key) } for (libraryId, keys) in groupedSettings { diff --git a/ZoteroTests/SyncActionsSpec.swift b/ZoteroTests/SyncActionsSpec.swift index a50f60f09..2cc367106 100644 --- a/ZoteroTests/SyncActionsSpec.swift +++ b/ZoteroTests/SyncActionsSpec.swift @@ -740,6 +740,48 @@ final class SyncActionsSpec: QuickSpec { }) } } + + context("remote settings deletions") { + it("doesn't treat lastReadAloudPosition deletions as lastRead deletions") { + try! realm.write { + let lastRead = RLastReadDate() + lastRead.key = "EXISTINGKEY" + lastRead.date = Date(timeIntervalSince1970: 1234) + lastRead.groupKey = 1 + realm.add(lastRead) + } + + waitUntil(timeout: .seconds(60), action: { doneAction in + PerformDeletionsSyncAction( + libraryId: .custom(.myLibrary), + collections: [], + items: [], + searches: [], + tags: [], + settings: ["lastReadAloudPosition_u_R2NCC4YU"], + conflictMode: .resolveConflicts, + dbStorage: dbStorage, + queue: .main + ) + .result + .subscribe(onSuccess: { result in + expect(result.conflicts).to(beEmpty()) + expect(result.unexpectedMyLibraryLastReadDeletions).to(beEmpty()) + + realm.refresh() + let lastReadDates = realm.objects(RLastReadDate.self) + expect(lastReadDates.count).to(equal(1)) + expect(lastReadDates.first?.key).to(equal("EXISTINGKEY")) + + doneAction() + }, onFailure: { error in + fail("PerformDeletionsSyncAction failed with \(error)") + doneAction() + }) + .disposed(by: disposeBag) + }) + } + } } } }