diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 7acf4816b..8f542410c 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -614,8 +614,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, delegate.addWebFeed(for: self, with: feed, to: container, completion: completion) } - public func createWebFeed(url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { - delegate.createWebFeed(for: self, url: url, name: name, container: container, completion: completion) + public func createWebFeed(url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { + delegate.createWebFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion) } func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> WebFeed { diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index 913f59dd9..f86368053 100644 --- a/Account/Sources/Account/AccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegate.swift @@ -35,7 +35,7 @@ protocol AccountDelegate { func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) - func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) + func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result) -> Void) func addWebFeed(for account: Account, with: WebFeed, to container: Container, completion: @escaping (Result) -> Void) func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result) -> Void) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index e5b9bc84d..88ce852f6 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -157,7 +157,7 @@ final class CloudKitAccountDelegate: AccountDelegate { } - func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { guard let url = URL(string: urlString), let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { completion(.failure(LocalAccountDelegateError.invalidParameter)) return @@ -169,7 +169,7 @@ final class CloudKitAccountDelegate: AccountDelegate { if let feedProvider = FeedProviderManager.shared.best(for: urlComponents) { createProviderWebFeed(for: account, urlComponents: urlComponents, editedName: editedName, container: container, feedProvider: feedProvider, completion: completion) } else { - createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, completion: completion) + createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion) } } @@ -199,7 +199,7 @@ final class CloudKitAccountDelegate: AccountDelegate { completion(.success(())) case .failure(let error): switch error { - case CloudKitZoneError.invalidParameter: + case CloudKitZoneError.corruptAccount: // We got into a bad state and should remove the feed to clear up the bad data account.clearWebFeedMetadata(feed) container.removeWebFeed(feed) @@ -242,7 +242,7 @@ final class CloudKitAccountDelegate: AccountDelegate { } func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result) -> Void) { - createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in switch result { case .success: completion(.success(())) @@ -310,20 +310,22 @@ final class CloudKitAccountDelegate: AccountDelegate { } group.notify(queue: DispatchQueue.global(qos: .background)) { - guard !errorOccurred else { - self.refreshProgress.completeTask() - completion(.failure(CloudKitAccountDelegateError.unknown)) - return - } - - self.accountZone.removeFolder(folder) { result in - self.refreshProgress.completeTask() - switch result { - case .success: - account.removeFolder(folder) - completion(.success(())) - case .failure(let error): - completion(.failure(error)) + DispatchQueue.main.async { + guard !errorOccurred else { + self.refreshProgress.completeTask() + completion(.failure(CloudKitAccountDelegateError.unknown)) + return + } + + self.accountZone.removeFolder(folder) { result in + self.refreshProgress.completeTask() + switch result { + case .success: + account.removeFolder(folder) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } } } } @@ -636,7 +638,9 @@ private extension CloudKitAccountDelegate { account.update(urlString, with: parsedItems) { result in switch result { case .success: - self.sendNewArticlesToTheCloud(account, feed, completion: completion) + self.sendNewArticlesToTheCloud(account, feed) + self.refreshProgress.clear() + completion(.success(feed)) case .failure(let error): self.refreshProgress.completeTasks(2) completion(.failure(error)) @@ -657,14 +661,36 @@ private extension CloudKitAccountDelegate { } case .failure: - self.refreshProgress.completeTasks(5) + self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorNotFound)) } } } - func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result) -> Void) { - BatchUpdate.shared.start() + func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { + + func addDeadFeed() { + let feed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil) + container.addWebFeed(feed) + + self.accountZone.createWebFeed(url: url.absoluteString, + name: editedName, + editedName: nil, + homePageURL: nil, + container: container) { result in + + self.refreshProgress.completeTask() + switch result { + case .success(let externalID): + feed.externalID = externalID + completion(.success(feed)) + case .failure(let error): + container.removeWebFeed(feed) + completion(.failure(error)) + } + } + } + refreshProgress.addToNumberOfTasksAndRemaining(5) FeedFinder.find(url: url) { result in @@ -672,15 +698,18 @@ private extension CloudKitAccountDelegate { switch result { case .success(let feedSpecifiers): guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else { - BatchUpdate.shared.end() - self.refreshProgress.completeTasks(5) - completion(.failure(AccountError.createErrorNotFound)) + self.refreshProgress.completeTasks(3) + if validateFeed { + self.refreshProgress.completeTask() + completion(.failure(AccountError.createErrorNotFound)) + } else { + addDeadFeed() + } return } if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) { - BatchUpdate.shared.end() - self.refreshProgress.completeTasks(5) + self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorAlreadySubscribed)) return } @@ -696,7 +725,6 @@ private extension CloudKitAccountDelegate { account.update(feed, with: parsedFeed) { result in switch result { case .success: - BatchUpdate.shared.end() self.accountZone.createWebFeed(url: bestFeedSpecifier.urlString, name: parsedFeed.title, @@ -708,7 +736,8 @@ private extension CloudKitAccountDelegate { switch result { case .success(let externalID): feed.externalID = externalID - self.sendNewArticlesToTheCloud(account, feed, completion: completion) + self.sendNewArticlesToTheCloud(account, feed) + completion(.success(feed)) case .failure(let error): container.removeWebFeed(feed) self.refreshProgress.completeTasks(2) @@ -718,7 +747,6 @@ private extension CloudKitAccountDelegate { } case .failure(let error): - BatchUpdate.shared.end() self.refreshProgress.completeTasks(3) completion(.failure(error)) } @@ -732,15 +760,19 @@ private extension CloudKitAccountDelegate { } case .failure: - BatchUpdate.shared.end() - self.refreshProgress.completeTasks(4) - completion(.failure(AccountError.createErrorNotFound)) + self.refreshProgress.completeTasks(3) + if validateFeed { + self.refreshProgress.completeTask() + completion(.failure(AccountError.createErrorNotFound)) + return + } else { + addDeadFeed() + } } - } } - func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed, completion: @escaping (Result) -> Void) { + func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed) { account.fetchArticlesAsync(.webFeed(feed)) { result in switch result { case .success(let articles): @@ -749,19 +781,14 @@ private extension CloudKitAccountDelegate { self.sendArticleStatus(for: account, showProgress: true) { result in switch result { case .success: - self.articlesZone.fetchChangesInZone() { _ in - self.refreshProgress.completeTask() - completion(.success(feed)) - } + self.articlesZone.fetchChangesInZone() { _ in } case .failure(let error): - self.refreshProgress.clear() - completion(.failure(error)) + os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription) } } } case .failure(let error): - self.refreshProgress.clear() - completion(.failure(error)) + os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription) } } } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index 64c99794f..fa58df337 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -103,7 +103,7 @@ final class CloudKitAccountZone: CloudKitZone { } guard let containerExternalID = container.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } record[CloudKitWebFeed.Fields.containerExternalIDs] = [containerExternalID] @@ -121,7 +121,7 @@ final class CloudKitAccountZone: CloudKitZone { /// Rename the given web feed func renameWebFeed(_ webFeed: WebFeed, editedName: String?, completion: @escaping (Result) -> Void) { guard let externalID = webFeed.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -142,7 +142,7 @@ final class CloudKitAccountZone: CloudKitZone { /// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted func removeWebFeed(_ webFeed: WebFeed, from: Container, completion: @escaping (Result) -> Void) { guard let fromContainerExternalID = from.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -187,7 +187,7 @@ final class CloudKitAccountZone: CloudKitZone { func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result) -> Void) { guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -209,7 +209,7 @@ final class CloudKitAccountZone: CloudKitZone { func addWebFeed(_ webFeed: WebFeed, to: Container, completion: @escaping (Result) -> Void) { guard let toContainerExternalID = to.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -292,7 +292,7 @@ final class CloudKitAccountZone: CloudKitZone { func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result) -> Void) { guard let externalID = folder.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index 095ddcdea..5c8733317 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -25,6 +25,8 @@ final class CloudKitArticlesZone: CloudKitZone { weak var database: CKDatabase? var delegate: CloudKitZoneDelegate? = nil + var compressionQueue = DispatchQueue(label: "Articles Zone Compression Queue") + struct CloudKitArticle { static let recordType = "Article" struct Fields { @@ -33,7 +35,9 @@ final class CloudKitArticlesZone: CloudKitZone { static let uniqueID = "uniqueID" static let title = "title" static let contentHTML = "contentHTML" + static let contentHTMLData = "contentHTMLData" static let contentText = "contentText" + static let contentTextData = "contentTextData" static let url = "url" static let externalURL = "externalURL" static let summary = "summary" @@ -96,7 +100,10 @@ final class CloudKitArticlesZone: CloudKitZone { records.append(makeArticleRecord(saveArticle)) } - save(records, completion: completion) + compressionQueue.async { + let compressedRecords = self.compressArticleRecords(records) + self.save(compressedRecords, completion: completion) + } } func deleteArticles(_ webFeedExternalID: String, completion: @escaping ((Result) -> Void)) { @@ -130,22 +137,27 @@ final class CloudKitArticlesZone: CloudKitZone { deleteRecordIDs.append(CKRecord.ID(recordName: self.articleID(statusUpdate.articleID), zoneID: zoneID)) } } - - self.modify(recordsToSave: modifyRecords, recordIDsToDelete: deleteRecordIDs) { result in - switch result { - case .success: - self.saveIfNew(newRecords) { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) + + compressionQueue.async { + let compressedModifyRecords = self.compressArticleRecords(modifyRecords) + self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in + switch result { + case .success: + let compressedNewRecords = self.compressArticleRecords(newRecords) + self.saveIfNew(compressedNewRecords) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } } + case .failure(let error): + self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) } - case .failure(let error): - self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) } } + } } @@ -237,5 +249,35 @@ private extension CloudKitArticlesZone { return record } + func compressArticleRecords(_ records: [CKRecord]) -> [CKRecord] { + var result = [CKRecord]() + + for record in records { + + if record.recordType == CloudKitArticle.recordType { + + if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String { + let data = Data(contentHTML.utf8) as NSData + if let compressedData = try? data.compressed(using: .lzfse) { + record[CloudKitArticle.Fields.contentHTMLData] = compressedData as Data + record[CloudKitArticle.Fields.contentHTML] = nil + } + } + + if let contentText = record[CloudKitArticle.Fields.contentText] as? String { + let data = Data(contentText.utf8) as NSData + if let compressedData = try? data.compressed(using: .lzfse) { + record[CloudKitArticle.Fields.contentTextData] = compressedData as Data + record[CloudKitArticle.Fields.contentText] = nil + } + } + + } + + result.append(record) + } + + return result + } } diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift index c3801c1ec..2f20b4204 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -23,6 +23,7 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate { weak var account: Account? var database: SyncDatabase weak var articlesZone: CloudKitArticlesZone? + var compressionQueue = DispatchQueue(label: "Articles Zone Delegate Compression Queue") init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone) { self.account = account @@ -134,7 +135,7 @@ private extension CloudKitArticlesZoneDelegate { } group.enter() - DispatchQueue.global(qos: .utility).async { + compressionQueue.async { let parsedItems = records.compactMap { self.makeParsedItem($0) } let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) } @@ -199,6 +200,20 @@ private extension CloudKitArticlesZoneDelegate { return nil } + var contentHTML = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTML] as? String + if let contentHTMLData = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTMLData] as? NSData { + if let decompressedContentHTMLData = try? contentHTMLData.decompressed(using: .lzfse) { + contentHTML = String(data: decompressedContentHTMLData as Data, encoding: .utf8) + } + } + + var contentText = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentText] as? String + if let contentTextData = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentTextData] as? NSData { + if let decompressedContentTextData = try? contentTextData.decompressed(using: .lzfse) { + contentText = String(data: decompressedContentTextData as Data, encoding: .utf8) + } + } + let parsedItem = ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: webFeedURL, @@ -206,8 +221,8 @@ private extension CloudKitArticlesZoneDelegate { externalURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.externalURL] as? String, title: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.title] as? String, language: nil, - contentHTML: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTML] as? String, - contentText: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentText] as? String, + contentHTML: contentHTML, + contentText: contentText, summary: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.summary] as? String, imageURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.imageURL] as? String, bannerImageURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.imageURL] as? String, diff --git a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift index 30c983ef2..26497e916 100644 --- a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -16,7 +16,7 @@ import SyncDatabase class CloudKitSendStatusOperation: MainThreadOperation { private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") - private let blockSize = 300 + private let blockSize = 150 // MainThreadOperation public var isCanceled = false diff --git a/Account/Sources/Account/FeedWrangler/FeedWranglerAccountDelegate.swift b/Account/Sources/Account/FeedWrangler/FeedWranglerAccountDelegate.swift index 7e44c6e1d..06edc663f 100644 --- a/Account/Sources/Account/FeedWrangler/FeedWranglerAccountDelegate.swift +++ b/Account/Sources/Account/FeedWrangler/FeedWranglerAccountDelegate.swift @@ -297,7 +297,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate { fatalError() } - func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { refreshProgress.addToNumberOfTasksAndRemaining(2) self.refreshCredentials(for: account) { @@ -439,7 +439,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate { } } } else { - createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in switch result { case .success: completion(.success(())) diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift index 00fa3ea19..406a34218 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift @@ -370,7 +370,7 @@ final class FeedbinAccountDelegate: AccountDelegate { } - func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { refreshProgress.addToNumberOfTasksAndRemaining(1) caller.createSubscription(url: url) { result in @@ -496,7 +496,7 @@ final class FeedbinAccountDelegate: AccountDelegate { } } } else { - createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in switch result { case .success: completion(.success(())) @@ -998,7 +998,7 @@ private extension FeedbinAccountDelegate { } if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) { - createWebFeed(for: account, url: bestSpecifier.urlString, name: name, container: container, completion: completion) + createWebFeed(for: account, url: bestSpecifier.urlString, name: name, container: container, validateFeed: true, completion: completion) } else { DispatchQueue.main.async { completion(.failure(FeedbinAccountDelegateError.invalidParameter)) diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index 635bbe0bc..81fe6e6a5 100644 --- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -369,7 +369,9 @@ final class FeedlyAPICaller { } } - send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + // `resultType` is optional because the Feedly API has gone from returning an array of removed feeds to returning `null`. + // https://developer.feedly.com/v3/collections/#remove-multiple-feeds-from-a-personal-collection + send(request: request, resultType: Optional<[FeedlyFeed]>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success((let httpResponse, _)): if httpResponse.statusCode == 200 { diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 558b1ba5b..56b7533d1 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -296,7 +296,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } } - func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { do { guard let credentials = credentials else { @@ -450,7 +450,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } } } else { - createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in switch result { case .success: completion(.success(())) diff --git a/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift b/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift index ac5a3d08d..2ae29f7f7 100644 --- a/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Account/Sources/Account/LocalAccount/LocalAccountDelegate.swift @@ -146,7 +146,7 @@ final class LocalAccountDelegate: AccountDelegate { } - func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { guard let url = URL(string: urlString), let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { completion(.failure(LocalAccountDelegateError.invalidParameter)) return diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift index e0b5f2f6f..71f4f22b3 100644 --- a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift @@ -405,7 +405,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> ()) { + func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> ()) { refreshProgress.addToNumberOfTasksAndRemaining(1) let folderName = (container as? Folder)?.name @@ -512,7 +512,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } } else { - createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in switch result { case .success: completion(.success(())) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 46e66b540..182db4203 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -326,7 +326,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } - func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { guard let url = URL(string: url) else { completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) return @@ -489,7 +489,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } } } else { - createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in switch result { case .success: completion(.success(())) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index 1d4845fdb..4e9f1b0c1 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -117,7 +117,9 @@ final class ReaderAPICaller: NSObject { var authData: [String: String] = [:] rawData.split(separator: "\n").forEach({ (line: Substring) in let items = line.split(separator: "=").map{String($0)} - authData[items[0]] = items[1] + if items.count == 2 { + authData[items[0]] = items[1] + } }) guard let authString = authData["Auth"] else { diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 8aff09137..00fee7fb7 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -43,7 +43,7 @@ final class AppDefaults { static let showDebugMenu = "ShowDebugMenu" static let timelineShowsSeparators = "CorreiaSeparators" static let showTitleOnMainWindow = "KafasisTitleMode" - static let hideDockUnreadCount = "JustinMillerHideDockUnreadCount" + static let feedDoubleClickMarkAsRead = "GruberFeedDoubleClickMarkAsRead" #if !MAC_APP_STORE static let webInspectorEnabled = "WebInspectorEnabled" @@ -194,12 +194,12 @@ final class AppDefaults { return AppDefaults.bool(for: Key.showDebugMenu) } - var hideDockUnreadCount: Bool { + var feedDoubleClickMarkAsRead: Bool { get { - return AppDefaults.bool(for: Key.hideDockUnreadCount) + return AppDefaults.bool(for: Key.feedDoubleClickMarkAsRead) } set { - AppDefaults.setBool(for: Key.hideDockUnreadCount, newValue) + AppDefaults.setBool(for: Key.feedDoubleClickMarkAsRead, newValue) } } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 23b5e819c..9b80e8b54 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -49,6 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, var refreshTimer: AccountRefreshTimer? var syncTimer: ArticleStatusSyncTimer? + var lastRefreshInterval = AppDefaults.shared.refreshInterval var shuttingDown = false { didSet { @@ -246,10 +247,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenter.current().delegate = self userNotificationManager = UserNotificationManager() + #if DEBUG + refreshTimer!.update() + syncTimer!.update() + #else + DispatchQueue.main.async { + self.refreshTimer!.timedRefresh(nil) + self.syncTimer!.timedRefresh(nil) + } + #endif + if AppDefaults.shared.showDebugMenu { - refreshTimer!.update() - syncTimer!.update() - // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build. #if MAC_APP_STORE let debugMenu = debugMenuItem.submenu! @@ -260,10 +268,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, #endif } else { debugMenuItem.menu?.removeItem(debugMenuItem) - DispatchQueue.main.async { - self.refreshTimer!.timedRefresh(nil) - self.syncTimer!.timedRefresh(nil) - } } #if !MAC_APP_STORE @@ -347,7 +351,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @objc func userDefaultsDidChange(_ note: Notification) { updateSortMenuItems() updateGroupByFeedMenuItem() - refreshTimer?.update() + + if lastRefreshInterval != AppDefaults.shared.refreshInterval { + refreshTimer?.update() + lastRefreshInterval = AppDefaults.shared.refreshInterval + } + updateDockBadge() } @@ -491,7 +500,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: - Dock Badge @objc func updateDockBadge() { - let label = unreadCount > 0 && !AppDefaults.shared.hideDockUnreadCount ? "\(unreadCount)" : "" + let label = unreadCount > 0 ? "\(unreadCount)" : "" NSApplication.shared.dockTile.badgeLabel = label } @@ -608,7 +617,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBAction func openWebsite(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/", inBackground: false) + Browser.open("https://netnewswire.com/", inBackground: false) } @IBAction func openReleaseNotes(_ sender: Any?) { @@ -632,7 +641,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func openSlackGroup(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/slack", inBackground: false) + Browser.open("https://netnewswire.com/slack", inBackground: false) } @IBAction func openTechnotes(_ sender: Any?) { @@ -642,7 +651,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBAction func showHelp(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/help/mac/5.1/en/", inBackground: false) + Browser.open("https://netnewswire.com/help/mac/6.0/en/", inBackground: false) } @IBAction func donateToAppCampForGirls(_ sender: Any?) { @@ -650,7 +659,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func showPrivacyPolicy(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/privacypolicy", inBackground: false) + Browser.open("https://netnewswire.com/privacypolicy", inBackground: false) } @IBAction func gotoToday(_ sender: Any?) { @@ -715,9 +724,11 @@ extension AppDelegate { } @IBAction func debugTestCrashReportSending(_ sender: Any?) { - #if DEBUG - CrashReporter.sendCrashLogText("This is a test. Hi, Brent.") - #endif + CrashReporter.sendCrashLogText("This is a test. Hi, Brent.") + } + + @IBAction func forceCrash(_ sender: Any?) { + fatalError("This is a deliberate crash.") } @IBAction func openApplicationSupportFolder(_ sender: Any?) { diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 9e3bdf707..a1f665aa2 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -516,6 +516,7 @@ + @@ -528,6 +529,13 @@ + + + + + + + @@ -682,7 +690,7 @@ - + diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard index 25b430794..ddd693d02 100644 --- a/Mac/Base.lproj/Preferences.storyboard +++ b/Mac/Base.lproj/Preferences.storyboard @@ -31,15 +31,15 @@ - - + + - + - + @@ -47,7 +47,7 @@ - + @@ -76,10 +76,10 @@ - + - + @@ -87,7 +87,7 @@ - + @@ -102,7 +102,7 @@ - + @@ -130,10 +130,10 @@ - + - + @@ -141,7 +141,7 @@ - + @@ -175,37 +175,16 @@ - - - - - - - - - - - - @@ -219,18 +198,15 @@ - - - - + @@ -243,7 +219,6 @@ - @@ -256,14 +231,14 @@ - + - + - + @@ -271,7 +246,7 @@