diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 6565c0846..4e5f3890a 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -304,7 +304,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 705424ca4..526c29049 100644 --- a/Frameworks/Account/AccountMetadata.swift +++ b/Frameworks/Account/AccountMetadata.swift @@ -24,6 +24,8 @@ final class AccountMetadata: Codable { case lastArticleFetchEndTime case endpointURL case externalID + case lastCredentialRenewTime = "lastCredentialRenewTime" + case performedApril2020RetentionPolicyChange } var name: String? { @@ -81,6 +83,24 @@ 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) + } + } + } + + var performedApril2020RetentionPolicyChange: Bool? { + didSet { + if performedApril2020RetentionPolicyChange != oldValue { + valueDidChange(.performedApril2020RetentionPolicyChange) + } + } + } var externalID: String? { didSet { 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/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 47990fdf4..61dba4f39 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -116,11 +116,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() @@ -130,9 +139,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)) { @@ -160,7 +169,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 @@ -168,7 +177,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 @@ -497,9 +506,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 4caccae47..200554ed3 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -90,7 +90,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 43d9d2507..09968d750 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift @@ -30,8 +30,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 e46a26e82..8c0ba5b03 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift @@ -26,8 +26,8 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { private var remoteEntryIds = Set() 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 48fa7d9c6..02dc9af61 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift @@ -29,8 +29,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 243502fbd..a06ebe0f5 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift @@ -27,8 +27,8 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { private var remoteEntryIds = Set() 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 ad8271d12..096ec5c5e 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift @@ -18,14 +18,32 @@ 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 { + os_log(.debug, log: log, "Skipping access token renewal.") + didFinish() + return + } + let refreshToken: Credentials do { @@ -65,6 +83,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) diff --git a/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift index 34d6b3677..6cbde0443 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlySyncAllOperation.swift @@ -34,7 +34,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() @@ -54,7 +54,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) @@ -68,14 +68,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 @@ -83,14 +83,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) @@ -126,8 +126,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() { diff --git a/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift b/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift index 71091352d..02935e1d9 100644 --- a/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift +++ b/Frameworks/Account/NewsBlur/NewsBlurAPICaller.swift @@ -38,7 +38,7 @@ final class NewsBlurAPICaller: NSObject { func validateCredentials(completion: @escaping (Result) -> 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 { @@ -228,7 +228,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)) diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 595343266..51e382578 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -156,7 +156,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 @@ -177,7 +177,7 @@ public final class ArticlesDatabase { /// Fetch non-zero unread counts for given webFeedIDs. public func fetchUnreadCounts(for webFeedIDs: Set, _ 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) @@ -275,14 +275,35 @@ 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) { if retentionStyle == .syncSystem { articlesTable.deleteOldArticles() } 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() } } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index d6e076e2e..e69a9def7 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. @@ -493,7 +493,9 @@ 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. + /// + /// Returns `true` if it deleted old articles all the way up to the 90 day cutoff date. func deleteOldArticles() { precondition(retentionStyle == .syncSystem) @@ -525,6 +527,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 = "delete from statuses where dateArrived Set
{ - // 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
{ + 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
{ let sql = "select rowid from search where search match ?;" let sqlSearchString = sqliteSearchString(with: searchString) @@ -688,7 +710,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 { @@ -761,7 +783,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, _ database: FMDatabase) -> Set
{ @@ -772,11 +794,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
{ - 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, _ database: FMDatabase) -> Set
{ @@ -786,7 +808,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, _ cutoffDate: Date, _ database: FMDatabase) -> Set
{ @@ -799,7 +821,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, _ database: FMDatabase) -> Set
{ @@ -810,7 +832,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, _ database: FMDatabase) -> Set
{ 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 - init(webFeedIDs: Set, databaseQueue: DatabaseQueue, cutoffDate: Date) { + init(webFeedIDs: Set, 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() diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 04a998ca9..a1c197918 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -347,15 +347,15 @@ -