From 8973adbdbdc9cb85cf9c3f1b0a1e3a6a3816f899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiel=20Gillard=20=F0=9F=A4=AA?= <kgillard@deputy.com> Date: Sun, 19 Apr 2020 08:31:20 +1000 Subject: [PATCH 1/9] Add lastCredentialRenewTime and honour it in FeedlyRefreshAccessTokenOperation --- Frameworks/Account/AccountMetadata.swift | 11 ++++++++++ .../FeedlyRefreshAccessTokenOperation.swift | 21 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Frameworks/Account/AccountMetadata.swift b/Frameworks/Account/AccountMetadata.swift index 7c5f378f9..f4c50ab44 100644 --- a/Frameworks/Account/AccountMetadata.swift +++ b/Frameworks/Account/AccountMetadata.swift @@ -23,6 +23,7 @@ final class AccountMetadata: Codable { case lastArticleFetchStartTime = "lastArticleFetch" case lastArticleFetchEndTime case endpointURL + case lastCredentialRenewTime = "lastCredentialRenewTime" } var name: String? { @@ -80,6 +81,16 @@ final class AccountMetadata: Codable { } } } + + /// The last moment an account successfully renewed its credentials, or `nil` if no such moment exists. + /// An account delegate can use this value to decide when to next ask the service provider to renew credentials. + var lastCredentialRenewTime: Date? { + didSet { + if lastCredentialRenewTime != oldValue { + valueDidChange(.lastCredentialRenewTime) + } + } + } weak var delegate: AccountMetadataDelegate? diff --git a/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift index d1c68d970..d6412f202 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift @@ -17,14 +17,31 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation { let account: Account let log: OSLog - init(account: Account, service: OAuthAccessTokenRefreshing, oauthClient: OAuthAuthorizationClient, log: OSLog) { + /// The moment the refresh is being requested. The token will refresh only if the account's `lastCredentialRenewTime` is not on the same day as this moment. When nil, the operation will always refresh the token. + let refreshDate: Date? + + init(account: Account, service: OAuthAccessTokenRefreshing, oauthClient: OAuthAuthorizationClient, refreshDate: Date?, log: OSLog) { self.oauthClient = oauthClient self.service = service self.account = account + self.refreshDate = refreshDate self.log = log } override func run() { + // Only refresh the token if these dates are not on the same day. + let shouldRefresh: Bool = { + guard let date = refreshDate, let lastRenewDate = account.metadata.lastCredentialRenewTime else { + return true + } + return !Calendar.current.isDate(lastRenewDate, equalTo: date, toGranularity: .day) + }() + + guard shouldRefresh else { + didFinish() + return + } + let refreshToken: Credentials do { @@ -64,6 +81,8 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation { // Now store the access token because we want the account delegate to use it. try account.storeCredentials(grant.accessToken) + account.metadata.lastCredentialRenewTime = Date() + didFinish() } catch { didFinish(with: error) From 5ab13ae705ba75540bf3947411ee0a7c9e44d2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiel=20Gillard=20=F0=9F=A4=AA?= <kgillard@deputy.com> Date: Sun, 19 Apr 2020 08:45:51 +1000 Subject: [PATCH 2/9] Ensure token is refreshed at least once a day before syncing. --- .../Feedly/FeedlyAccountDelegate.swift | 26 ++++++++++++------- .../FeedlyAddNewFeedOperation.swift | 2 +- .../FeedlyGetUpdatedArticleIdsOperation.swift | 4 +-- ...edlyIngestStarredArticleIdsOperation.swift | 4 +-- ...eedlyIngestStreamArticleIdsOperation.swift | 4 +-- ...eedlyIngestUnreadArticleIdsOperation.swift | 4 +-- .../FeedlyRefreshAccessTokenOperation.swift | 1 + .../Operations/FeedlySyncAllOperation.swift | 16 ++++++------ 8 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 369e237f8..184ea576b 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -111,11 +111,20 @@ final class FeedlyAccountDelegate: AccountDelegate { } let log = self.log - let operation = FeedlySyncAllOperation(account: account, credentials: credentials, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress, log: log) - operation.downloadProgress = refreshProgress + let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, refreshDate: Date(), log: log) + refreshAccessToken.downloadProgress = refreshProgress + operationQueue.add(refreshAccessToken) + + let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserId: credentials.username, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress, log: log) + + syncAllOperation.downloadProgress = refreshProgress + + // Ensure the sync uses the latest credential. + syncAllOperation.addDependency(refreshAccessToken) + let date = Date() - operation.syncCompletionHandler = { [weak self] result in + syncAllOperation.syncCompletionHandler = { [weak self] result in if case .success = result { self?.accountMetadata?.lastArticleFetchStartTime = date self?.accountMetadata?.lastArticleFetchEndTime = Date() @@ -125,9 +134,9 @@ final class FeedlyAccountDelegate: AccountDelegate { completion(result) } - currentSyncAllOperation = operation + currentSyncAllOperation = syncAllOperation - operationQueue.add(operation) + operationQueue.add(syncAllOperation) } func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) { @@ -155,7 +164,7 @@ final class FeedlyAccountDelegate: AccountDelegate { let group = DispatchGroup() - let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: caller, database: database, newerThan: nil, log: log) + let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: credentials.username, service: caller, database: database, newerThan: nil, log: log) group.enter() ingestUnread.completionBlock = { _ in @@ -163,7 +172,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } - let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: caller, database: database, newerThan: nil, log: log) + let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, userId: credentials.username, service: caller, database: database, newerThan: nil, log: log) group.enter() ingestStarred.completionBlock = { _ in @@ -492,9 +501,6 @@ final class FeedlyAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { credentials = try? account.retrieveCredentials(type: .oauthAccessToken) - - let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log) - operationQueue.add(refreshAccessToken) } func accountWillBeDeleted(_ account: Account) { diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift index e3641511e..d4852c2f4 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -89,7 +89,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl createFeeds.downloadProgress = downloadProgress operationQueue.add(createFeeds) - let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, database: database, newerThan: nil, log: log) + let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: credentials.username, service: syncUnreadIdsService, database: database, newerThan: nil, log: log) syncUnread.addDependency(createFeeds) syncUnread.downloadProgress = downloadProgress syncUnread.delegate = self diff --git a/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift index 1ed7a2c10..43ce44643 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift @@ -29,8 +29,8 @@ class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifie self.log = log } - convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { - let all = FeedlyCategoryResourceId.Global.all(for: credentials.username) + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { + let all = FeedlyCategoryResourceId.Global.all(for: userId) self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log) } diff --git a/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift index 02dc42043..7793315e0 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift @@ -25,8 +25,8 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { private var remoteEntryIds = Set<String>() private let log: OSLog - convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { - let resource = FeedlyTagResourceId.Global.saved(for: credentials.username) + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { + let resource = FeedlyTagResourceId.Global.saved(for: userId) self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log) } diff --git a/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift index 12b906d12..8e7716159 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift @@ -28,8 +28,8 @@ class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation { self.log = log } - convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, log: OSLog) { - let all = FeedlyCategoryResourceId.Global.all(for: credentials.username) + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, log: OSLog) { + let all = FeedlyCategoryResourceId.Global.all(for: userId) self.init(account: account, resource: all, service: service, log: log) } diff --git a/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift index 669a9672d..271caec65 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift @@ -26,8 +26,8 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { private var remoteEntryIds = Set<String>() private let log: OSLog - convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { - let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username) + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { + let resource = FeedlyCategoryResourceId.Global.all(for: userId) self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log) } diff --git a/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift index d6412f202..b4878dc49 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift @@ -38,6 +38,7 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation { }() guard shouldRefresh else { + os_log(.debug, log: log, "Skipping access token renewal.") didFinish() return } diff --git a/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift index b0c87e027..6a2e36a76 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift @@ -33,7 +33,7 @@ final class FeedlySyncAllOperation: FeedlyOperation { /// /// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync). /// - init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) { + init(account: Account, feedlyUserId: String, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) { self.syncUUID = UUID() self.log = log self.operationQueue.suspend() @@ -53,7 +53,7 @@ final class FeedlySyncAllOperation: FeedlyOperation { getCollections.delegate = self getCollections.downloadProgress = downloadProgress getCollections.addDependency(sendArticleStatuses) - self.operationQueue.add(getCollections) + self.operationQueue.add(getCollections) // Ensure a folder exists for each Collection, removing Folders without a corresponding Collection. let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: log) @@ -67,14 +67,14 @@ final class FeedlySyncAllOperation: FeedlyOperation { createFeedsOperation.addDependency(mirrorCollectionsAsFolders) self.operationQueue.add(createFeedsOperation) - let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, log: log) + let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, userId: feedlyUserId, service: getStreamIdsService, log: log) getAllArticleIds.delegate = self getAllArticleIds.downloadProgress = downloadProgress getAllArticleIds.addDependency(createFeedsOperation) self.operationQueue.add(getAllArticleIds) // Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default). - let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: getUnreadService, database: database, newerThan: nil, log: log) + let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: feedlyUserId, service: getUnreadService, database: database, newerThan: nil, log: log) getUnread.delegate = self getUnread.addDependency(getAllArticleIds) getUnread.downloadProgress = downloadProgress @@ -82,14 +82,14 @@ final class FeedlySyncAllOperation: FeedlyOperation { // Get each page of the article ids which have been update since the last successful fetch start date. // If the date is nil, this operation provides an empty set (everything is new, nothing is updated). - let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log) + let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, userId: feedlyUserId, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log) getUpdated.delegate = self getUpdated.downloadProgress = downloadProgress getUpdated.addDependency(createFeedsOperation) self.operationQueue.add(getUpdated) // Get each page of the article ids for starred articles. - let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: getStarredService, database: database, newerThan: nil, log: log) + let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, userId: feedlyUserId, service: getStarredService, database: database, newerThan: nil, log: log) getStarred.delegate = self getStarred.downloadProgress = downloadProgress getStarred.addDependency(createFeedsOperation) @@ -125,8 +125,8 @@ final class FeedlySyncAllOperation: FeedlyOperation { self.operationQueue.add(finishOperation) } - convenience init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) { - self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log) + convenience init(account: Account, feedlyUserId: String, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) { + self.init(account: account, feedlyUserId: feedlyUserId, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log) } override func run() { From d2812fa1321defde8febd8098b41ff2a67cca82a Mon Sep 17 00:00:00 2001 From: Brent Simmons <brent@ranchero.com> Date: Sat, 18 Apr 2020 16:59:13 -0700 Subject: [PATCH 3/9] Delete old statuses at startup. --- .../ArticlesDatabase/ArticlesDatabase.swift | 1 + .../ArticlesDatabase/ArticlesTable.swift | 26 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index aac8852db..4b16049d8 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -273,6 +273,7 @@ public final class ArticlesDatabase { articlesTable.deleteOldArticles() } articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs) + articlesTable.deleteOldStatuses() } } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index e87f699e8..68a31189f 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -493,7 +493,7 @@ final class ArticlesTable: DatabaseTable { /// Because deleting articles might block the database for too long, /// we do this in a careful way: delete articles older than a year, /// check to see how much time has passed, then decide whether or not to continue. - /// Repeat for successively shorter time intervals. + /// Repeat for successively more-recent dates. func deleteOldArticles() { precondition(retentionStyle == .syncSystem) @@ -525,6 +525,30 @@ final class ArticlesTable: DatabaseTable { } } + /// Delete old statuses. + func deleteOldStatuses() { + queue.runInTransaction { databaseResult in + guard let database = databaseResult.database else { + return + } + + let sql: String + let cutoffDate: Date + + switch self.retentionStyle { + case .syncSystem: + sql = "select articleID from statuses where dateArrived<? and read=1 and starred=0 and articleID not in (select articleID from articles);" + cutoffDate = Date().bySubtracting(days: 180) + case .feedBased: + sql = "select articleID from statuses where dateArrived<? and starred=0 and articleID not in (select articleID from articles);" + cutoffDate = Date().bySubtracting(days: 30) + } + + let parameters = [cutoffDate] as [Any] + database.executeUpdate(sql, withArgumentsIn: parameters) + } + } + /// Delete articles from feeds that are no longer in the current set of subscribed-to feeds. /// This deletes from the articles and articleStatuses tables, /// and, via a trigger, it also deletes from the search index. From b2b000dd2e581b6417be23c811c297987e562cc9 Mon Sep 17 00:00:00 2001 From: Brent Simmons <brent@ranchero.com> Date: Sat, 18 Apr 2020 20:18:32 -0700 Subject: [PATCH 4/9] Implement updated retention policy. --- .../ArticlesDatabase/ArticlesDatabase.swift | 12 +++-- .../ArticlesDatabase/ArticlesTable.swift | 48 ++++++------------- .../FetchAllUnreadCountsOperation.swift | 8 ++-- .../FetchFeedUnreadCountOperation.swift | 4 +- .../FetchUnreadCountsForFeedsOperation.swift | 10 ++-- 5 files changed, 30 insertions(+), 52 deletions(-) diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 4b16049d8..d0f3c424c 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -146,7 +146,7 @@ public final class ArticlesDatabase { /// Fetch all non-zero unread counts. public func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) { - let operation = FetchAllUnreadCountsOperation(databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate) + let operation = FetchAllUnreadCountsOperation(databaseQueue: queue) operationQueue.cancelOperations(named: operation.name!) operation.completionBlock = { operation in let fetchOperation = operation as! FetchAllUnreadCountsOperation @@ -167,7 +167,7 @@ public final class ArticlesDatabase { /// Fetch non-zero unread counts for given webFeedIDs. public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) { - let operation = FetchUnreadCountsForFeedsOperation(webFeedIDs: webFeedIDs, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate) + let operation = FetchUnreadCountsForFeedsOperation(webFeedIDs: webFeedIDs, databaseQueue: queue) operation.completionBlock = { operation in let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation completion(fetchOperation.result) @@ -265,9 +265,11 @@ public final class ArticlesDatabase { // MARK: - Cleanup - // These are to be used only at startup. These are to prevent the database from growing forever. - - /// Calls the various clean-up functions. + /// Calls the various clean-up functions. To be used only at startup. + /// + /// This prevents the database from growing forever. If we didn’t do this: + /// 1) The database would grow to an inordinate size, and + /// 2) the app would become very slow. public func cleanupDatabaseAtStartup(subscribedToWebFeedIDs: Set<String>) { if retentionStyle == .syncSystem { articlesTable.deleteOldArticles() diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 68a31189f..635d1d9b3 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -181,7 +181,7 @@ final class ArticlesTable: DatabaseTable { // 1. Ensure statuses for all the incoming articles. // 2. Create incoming articles with parsedItems. - // 3. [Deleted - no longer needed] + // 3. [Deleted - this step is no longer needed] // 4. Fetch all articles for the feed. // 5. Create array of Articles not in database and save them. // 6. Create array of updated Articles and save what’s changed. @@ -494,6 +494,8 @@ final class ArticlesTable: DatabaseTable { /// we do this in a careful way: delete articles older than a year, /// check to see how much time has passed, then decide whether or not to continue. /// Repeat for successively more-recent dates. + /// + /// Returns `true` if it deleted old articles all the way up to the 90 day cutoff date. func deleteOldArticles() { precondition(retentionStyle == .syncSystem) @@ -537,10 +539,10 @@ final class ArticlesTable: DatabaseTable { switch self.retentionStyle { case .syncSystem: - sql = "select articleID from statuses where dateArrived<? and read=1 and starred=0 and articleID not in (select articleID from articles);" + sql = "delete from statuses where dateArrived<? and read=1 and starred=0 and articleID not in (select articleID from articles);" cutoffDate = Date().bySubtracting(days: 180) case .feedBased: - sql = "select articleID from statuses where dateArrived<? and starred=0 and articleID not in (select articleID from articles);" + sql = "delete from statuses where dateArrived<? and starred=0 and articleID not in (select articleID from articles);" cutoffDate = Date().bySubtracting(days: 30) } @@ -672,31 +674,11 @@ private extension ArticlesTable { return cachedArticles.union(articlesWithFetchedAuthors) } - func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject], withLimits: Bool) -> Set<Article> { - // Don’t fetch articles that shouldn’t appear in the UI. The rules: - // * Must not be deleted. - // * Must be either 1) starred or 2) dateArrived must be newer than cutoff date. - - if withLimits { - let sql = "select * from articles natural join statuses where \(whereClause) and (starred=1 or dateArrived>?);" - return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database) - } - else { - let sql = "select * from articles natural join statuses where \(whereClause);" - return articlesWithSQL(sql, parameters, database) - } + func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]) -> Set<Article> { + let sql = "select * from articles natural join statuses where \(whereClause);" + return articlesWithSQL(sql, parameters, database) } -// func fetchUnreadCount(_ webFeedID: String, _ database: FMDatabase) -> Int { -// // Count only the articles that would appear in the UI. -// // * Must be unread. -// // * Must not be deleted. -// // * Must be either 1) starred or 2) dateArrived must be newer than cutoff date. -// -// let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and (starred=1 or dateArrived>?);" -// return numberWithSQLAndParameters(sql, [webFeedID, articleCutoffDate], in: database) -// } - func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> { let sql = "select rowid from search where search match ?;" let sqlSearchString = sqliteSearchString(with: searchString) @@ -712,7 +694,7 @@ private extension ArticlesTable { let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(searchRowIDs.count))! let whereClause = "searchRowID in \(placeholders)" let parameters: [AnyObject] = Array(searchRowIDs) as [AnyObject] - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } func sqliteSearchString(with searchString: String) -> String { @@ -785,7 +767,7 @@ private extension ArticlesTable { let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let whereClause = "feedID in \(placeholders)" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> { @@ -796,11 +778,11 @@ private extension ArticlesTable { let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let whereClause = "feedID in \(placeholders) and read=0" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } func fetchArticlesForFeedID(_ webFeedID: String, withLimits: Bool, _ database: FMDatabase) -> Set<Article> { - return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits) + return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject]) } func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> { @@ -810,7 +792,7 @@ private extension ArticlesTable { let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let whereClause = "articleID in \(placeholders)" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> { @@ -823,7 +805,7 @@ private extension ArticlesTable { let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject] let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } func fetchStarredArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> { @@ -834,7 +816,7 @@ private extension ArticlesTable { let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let whereClause = "feedID in \(placeholders) and starred=1" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> { diff --git a/Frameworks/ArticlesDatabase/Operations/FetchAllUnreadCountsOperation.swift b/Frameworks/ArticlesDatabase/Operations/FetchAllUnreadCountsOperation.swift index 2f82d2549..04c77d955 100644 --- a/Frameworks/ArticlesDatabase/Operations/FetchAllUnreadCountsOperation.swift +++ b/Frameworks/ArticlesDatabase/Operations/FetchAllUnreadCountsOperation.swift @@ -22,11 +22,9 @@ public final class FetchAllUnreadCountsOperation: MainThreadOperation { public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? private let queue: DatabaseQueue - private let cutoffDate: Date - init(databaseQueue: DatabaseQueue, cutoffDate: Date) { + init(databaseQueue: DatabaseQueue) { self.queue = databaseQueue - self.cutoffDate = cutoffDate } public func run() { @@ -49,9 +47,9 @@ public final class FetchAllUnreadCountsOperation: MainThreadOperation { private extension FetchAllUnreadCountsOperation { func fetchUnreadCounts(_ database: FMDatabase) { - let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and (starred=1 or dateArrived>?) group by feedID;" + let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 group by feedID;" - guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else { + guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { informOperationDelegateOfCompletion() return } diff --git a/Frameworks/ArticlesDatabase/Operations/FetchFeedUnreadCountOperation.swift b/Frameworks/ArticlesDatabase/Operations/FetchFeedUnreadCountOperation.swift index 4f8c99504..289bb73e6 100644 --- a/Frameworks/ArticlesDatabase/Operations/FetchFeedUnreadCountOperation.swift +++ b/Frameworks/ArticlesDatabase/Operations/FetchFeedUnreadCountOperation.swift @@ -52,9 +52,9 @@ public final class FetchFeedUnreadCountOperation: MainThreadOperation { private extension FetchFeedUnreadCountOperation { func fetchUnreadCount(_ database: FMDatabase) { - let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and (starred=1 or dateArrived>?);" + let sql = "select count(*) from articles natural join statuses where feedID=? and read=0;" - guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID, cutoffDate]) else { + guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID]) else { informOperationDelegateOfCompletion() return } diff --git a/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift b/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift index b323f4e4f..7f7533b77 100644 --- a/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift +++ b/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift @@ -23,13 +23,11 @@ public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation { public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? private let queue: DatabaseQueue - private let cutoffDate: Date private let webFeedIDs: Set<String> - init(webFeedIDs: Set<String>, databaseQueue: DatabaseQueue, cutoffDate: Date) { + init(webFeedIDs: Set<String>, databaseQueue: DatabaseQueue) { self.webFeedIDs = webFeedIDs self.queue = databaseQueue - self.cutoffDate = cutoffDate } public func run() { @@ -53,11 +51,9 @@ private extension FetchUnreadCountsForFeedsOperation { func fetchUnreadCounts(_ database: FMDatabase) { let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and (starred=1 or dateArrived>?) group by feedID;" + let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 group by feedID;" - var parameters = [Any]() - parameters += Array(webFeedIDs) as [Any] - parameters += [cutoffDate] as [Any] + let parameters = Array(webFeedIDs) as [Any] guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { informOperationDelegateOfCompletion() From 75d2158163a5eb6be74d109d9ef316c7fa53805a Mon Sep 17 00:00:00 2001 From: Brent Simmons <brent@ranchero.com> Date: Sun, 19 Apr 2020 14:10:12 -0700 Subject: [PATCH 5/9] =?UTF-8?q?Perform=20a=20one-time=20(per=20local=20acc?= =?UTF-8?q?ount)=20cleanup=20made=20necessary=20by=20the=20retention=20pol?= =?UTF-8?q?icy=20change=20=E2=80=94=C2=A0mark=20articles=20older=20than=20?= =?UTF-8?q?the=2090-day=20window=20as=20read.=20This=20way=20users=20won?= =?UTF-8?q?=E2=80=99t=20get=20a=20flood=20of=20old,=20unread=20articles=20?= =?UTF-8?q?when=20they=20run=20this=20new=20version.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frameworks/Account/Account.swift | 12 ++++++++++++ Frameworks/Account/AccountMetadata.swift | 9 +++++++++ .../ArticlesDatabase/ArticlesDatabase.swift | 18 ++++++++++++++++++ .../ArticlesDatabase/ArticlesTable.swift | 16 ++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 812ad9693..9824386d4 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -279,7 +279,19 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, webFeedMetadataFile.load() opmlFile.load() + var shouldHandleRetentionPolicyChange = false + if type == .onMyMac { + let didHandlePolicyChange = metadata.performedApril2020RetentionPolicyChange ?? false + shouldHandleRetentionPolicyChange = !didHandlePolicyChange + } + DispatchQueue.main.async { + if shouldHandleRetentionPolicyChange { + // Handle one-time database changes made necessary by April 2020 retention policy change. + self.database.performApril2020RetentionPolicyChange() + self.metadata.performedApril2020RetentionPolicyChange = true + } + self.database.cleanupDatabaseAtStartup(subscribedToWebFeedIDs: self.flattenedWebFeeds().webFeedIDs()) self.fetchAllUnreadCounts() } diff --git a/Frameworks/Account/AccountMetadata.swift b/Frameworks/Account/AccountMetadata.swift index f4c50ab44..9f5845a64 100644 --- a/Frameworks/Account/AccountMetadata.swift +++ b/Frameworks/Account/AccountMetadata.swift @@ -24,6 +24,7 @@ final class AccountMetadata: Codable { case lastArticleFetchEndTime case endpointURL case lastCredentialRenewTime = "lastCredentialRenewTime" + case performedApril2020RetentionPolicyChange } var name: String? { @@ -92,6 +93,14 @@ final class AccountMetadata: Codable { } } + var performedApril2020RetentionPolicyChange: Bool? { + didSet { + if performedApril2020RetentionPolicyChange != oldValue { + valueDidChange(.performedApril2020RetentionPolicyChange) + } + } + } + weak var delegate: AccountMetadataDelegate? func valueDidChange(_ key: CodingKeys) { diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index d0f3c424c..0ee215866 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -277,6 +277,24 @@ public final class ArticlesDatabase { articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs) articlesTable.deleteOldStatuses() } + + /// Do database cleanups made necessary by the retention policy change in April 2020. + /// + /// The retention policy for feed-based systems changed in April 2020: + /// we keep articles only for as long as they’re in the feed. + /// This change could result in a bunch of older articles suddenly + /// appearing as unread articles. + /// + /// These are articles that were in the database, + /// but weren’t appearing in the UI because they were beyond the 90-day window. + /// (The previous retention policy used a 90-day window.) + /// + /// This function marks everything as read that’s beyond that 90-day window. + /// It’s intended to be called only once on an account. + public func performApril2020RetentionPolicyChange() { + precondition(retentionStyle == .feedBased) + articlesTable.markOlderStatusesAsRead() + } } // MARK: - Private diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 635d1d9b3..ff36a8f9b 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -580,6 +580,22 @@ final class ArticlesTable: DatabaseTable { } } } + + /// Mark statuses beyond the 90-day window as read. + /// + /// This is not intended for wide use: this is part of implementing + /// the April 2020 retention policy change for feed-based accounts. + func markOlderStatusesAsRead() { + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + return + } + + let sql = "update statuses set read = true where dateArrived<?;" + let parameters = [self.articleCutoffDate] as [Any] + database.executeUpdate(sql, withArgumentsIn: parameters) + } + } } // MARK: - Private From 02ee0b25631f47207110a2d91c0680e2d3e4529c Mon Sep 17 00:00:00 2001 From: Brent Simmons <brent@ranchero.com> Date: Sun, 19 Apr 2020 19:12:34 -0700 Subject: [PATCH 6/9] Bump version to 5.0.1 build 40. --- xcconfig/common/NetNewsWire_ios_target_common.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index f7abdaac9..4a153915d 100644 --- a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig @@ -1,7 +1,7 @@ // High Level Settings common to both the iOS application and any extensions we bundle with it -MARKETING_VERSION = 5.0 -CURRENT_PROJECT_VERSION = 39 +MARKETING_VERSION = 5.0.1 +CURRENT_PROJECT_VERSION = 40 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon From 7afcf681fecc12a1bb2e4f5a987d58eed8962ab6 Mon Sep 17 00:00:00 2001 From: Maurice Parker <mo@vincode.io> Date: Tue, 21 Apr 2020 01:52:39 -0500 Subject: [PATCH 7/9] Stablize swipe back gesture for timeline. Issue #2002 --- iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift b/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift index fa9cda81e..8df7295bd 100644 --- a/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift +++ b/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift @@ -18,7 +18,7 @@ final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDele } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true + return navigationController?.viewControllers.count ?? 0 > 2 } } From f7bdf9d47cc9afa3115298043fbfb20b0c9167d1 Mon Sep 17 00:00:00 2001 From: Maurice Parker <mo@vincode.io> Date: Tue, 21 Apr 2020 01:57:19 -0500 Subject: [PATCH 8/9] Correct text resizing issues in color palette settings. Issue #2000 --- iOS/Settings/Settings.storyboard | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 1ecb90a8e..7983e3cb4 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="9cW-lu-HoC"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="9cW-lu-HoC"> <device id="retina6_1" orientation="portrait" appearance="light"/> <dependencies> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> <capability name="Named colors" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> @@ -320,21 +320,21 @@ <tableViewSection headerTitle="Appearance" id="TkH-4v-yhk"> <cells> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="EvG-yE-gDF" customClass="VibrantBasicTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="819.5" width="374" height="44"/> + <rect key="frame" x="20" y="819.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EvG-yE-gDF" id="wBN-zJ-6pN"> <rect key="frame" x="0.0" y="0.0" width="343" height="44"/> <autoresizingMask key="autoresizingMask"/> <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Color Palette" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2Fp-li-dGP"> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Color Palette" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2Fp-li-dGP"> <rect key="frame" x="20" y="11.5" width="99" height="21"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <nil key="textColor"/> <nil key="highlightedColor"/> </label> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Automatic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="16m-Ns-Y8V"> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Automatic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="16m-Ns-Y8V"> <rect key="frame" x="257" y="11.5" width="78" height="21"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <nil key="textColor"/> <nil key="highlightedColor"/> </label> @@ -356,7 +356,7 @@ <tableViewSection headerTitle="Help" id="CS8-fJ-ghn"> <cells> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="uGk-2d-oFc" style="IBUITableViewCellStyleDefault" id="Tle-IV-D40" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="919.5" width="374" height="44"/> + <rect key="frame" x="20" y="919.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Tle-IV-D40" id="IJD-ZB-8Wm"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -373,7 +373,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="6G3-yV-Eyh" style="IBUITableViewCellStyleDefault" id="Tbf-fE-nfx" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="963.5" width="374" height="44"/> + <rect key="frame" x="20" y="963.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Tbf-fE-nfx" id="beV-vI-g3r"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -390,7 +390,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="lfL-bQ-sOp" style="IBUITableViewCellStyleDefault" id="mFn-fE-zqa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="1007.5" width="374" height="44"/> + <rect key="frame" x="20" y="1007.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mFn-fE-zqa" id="jTe-mf-MRj"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -407,7 +407,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="DDJ-8P-3YY" style="IBUITableViewCellStyleDefault" id="iGs-ze-4gQ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="1051.5" width="374" height="44"/> + <rect key="frame" x="20" y="1051.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="iGs-ze-4gQ" id="EqZ-rF-N0l"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -424,7 +424,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="DsV-Qv-X4K" style="IBUITableViewCellStyleDefault" id="taJ-sg-wnU" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="1095.5" width="374" height="44"/> + <rect key="frame" x="20" y="1095.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="taJ-sg-wnU" id="axB-si-1KM"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -441,7 +441,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="zMz-hU-UYU" style="IBUITableViewCellStyleDefault" id="OXi-cg-ab9" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="1139.5" width="374" height="44"/> + <rect key="frame" x="20" y="1139.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="OXi-cg-ab9" id="npR-a0-9wv"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -458,7 +458,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="T7x-zl-6Yf" style="IBUITableViewCellStyleDefault" id="VpI-0o-3Px" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="1183.5" width="374" height="44"/> + <rect key="frame" x="20" y="1183.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VpI-0o-3Px" id="xRH-i4-vne"> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/> @@ -475,7 +475,7 @@ </tableViewCellContentView> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="NeD-y8-KrM" style="IBUITableViewCellStyleDefault" id="TIX-yK-rC6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> - <rect key="frame" x="0.0" y="1227.5" width="374" height="44"/> + <rect key="frame" x="20" y="1227.5" width="374" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="TIX-yK-rC6" id="qr8-EN-Ofg"> <rect key="frame" x="0.0" y="0.0" width="355" height="44"/> @@ -1021,10 +1021,10 @@ <rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/> <autoresizingMask key="autoresizingMask"/> <subviews> - <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6tf-Xb-7b8"> + <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="6tf-Xb-7b8"> <rect key="frame" x="20" y="0.0" width="334" height="43.5"/> <autoresizingMask key="autoresizingMask"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <nil key="textColor"/> <nil key="highlightedColor"/> </label> From fc374727573ba978621c947a6b45666b07c0f02c Mon Sep 17 00:00:00 2001 From: Maurice Parker <mo@vincode.io> Date: Tue, 21 Apr 2020 02:09:59 -0500 Subject: [PATCH 9/9] Fix source code warnings. --- Frameworks/Account/CloudKit/CloudKitArticlesZone.swift | 2 -- Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift b/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift index ae1ecab1d..1f71d3c46 100644 --- a/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift @@ -172,8 +172,6 @@ private extension CloudKitArticlesZone { record![CloudKitArticleStatus.Fields.read] = status.flag ? "1" : "0" case .starred: record![CloudKitArticleStatus.Fields.starred] = status.flag ? "1" : "0" - default: - break } } diff --git a/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift b/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift index 9c974b2cd..0437bb386 100644 --- a/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift +++ b/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift @@ -37,7 +37,7 @@ final class NewsBlurAPICaller: NSObject { func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) { requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in switch result { - case .success(let response, let payload): + case .success((let response, let payload)): guard let url = response.url, let headerFields = response.allHeaderFields as? [String: String], payload?.code != -1 else { let error = payload?.errors?.username ?? payload?.errors?.others if let message = error?.first { @@ -227,7 +227,7 @@ final class NewsBlurAPICaller: NSObject { resultType: NewsBlurAddURLResponse.self ) { result in switch result { - case .success(_, let payload): + case .success((_, let payload)): completion(.success(payload?.feed)) case .failure(let error): completion(.failure(error))