mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'ios-release'
This commit is contained in:
@@ -694,7 +694,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
database.fetchStarredArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion)
|
||||
}
|
||||
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date).
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date).
|
||||
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
||||
}
|
||||
|
||||
@@ -263,9 +263,6 @@ final class FeedWranglerAPICaller: NSObject {
|
||||
|
||||
case .starred:
|
||||
return URLQueryItem(name: "starred", value: status.flag.description)
|
||||
|
||||
case .userDeleted:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
queryItems.append(URLQueryItem(name: "feed_item_id", value: articleID))
|
||||
|
||||
@@ -19,7 +19,6 @@ public final class ArticleStatus: Hashable {
|
||||
public enum Key: String {
|
||||
case read = "read"
|
||||
case starred = "starred"
|
||||
case userDeleted = "userDeleted"
|
||||
}
|
||||
|
||||
public let articleID: String
|
||||
@@ -27,18 +26,16 @@ public final class ArticleStatus: Hashable {
|
||||
|
||||
public var read = false
|
||||
public var starred = false
|
||||
public var userDeleted = false
|
||||
|
||||
public init(articleID: String, read: Bool, starred: Bool, userDeleted: Bool, dateArrived: Date) {
|
||||
|
||||
public init(articleID: String, read: Bool, starred: Bool, dateArrived: Date) {
|
||||
self.articleID = articleID
|
||||
self.read = read
|
||||
self.starred = starred
|
||||
self.userDeleted = userDeleted
|
||||
self.dateArrived = dateArrived
|
||||
}
|
||||
|
||||
public convenience init(articleID: String, read: Bool, dateArrived: Date) {
|
||||
self.init(articleID: articleID, read: read, starred: false, userDeleted: false, dateArrived: dateArrived)
|
||||
self.init(articleID: articleID, read: read, starred: false, dateArrived: dateArrived)
|
||||
}
|
||||
|
||||
public func boolStatus(forKey key: ArticleStatus.Key) -> Bool {
|
||||
@@ -47,8 +44,6 @@ public final class ArticleStatus: Hashable {
|
||||
return read
|
||||
case .starred:
|
||||
return starred
|
||||
case .userDeleted:
|
||||
return userDeleted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +53,6 @@ public final class ArticleStatus: Hashable {
|
||||
read = status
|
||||
case .starred:
|
||||
starred = status
|
||||
case .userDeleted:
|
||||
userDeleted = status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +65,7 @@ public final class ArticleStatus: Hashable {
|
||||
// MARK: - Equatable
|
||||
|
||||
public static func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool {
|
||||
return lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred && lhs.userDeleted == rhs.userDeleted
|
||||
return lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ public final class ArticlesDatabase {
|
||||
articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion)
|
||||
}
|
||||
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date).
|
||||
/// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date).
|
||||
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
articlesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
||||
}
|
||||
@@ -293,7 +293,7 @@ private extension ArticlesDatabase {
|
||||
static let tableCreationStatements = """
|
||||
CREATE TABLE if not EXISTS articles (articleID TEXT NOT NULL PRIMARY KEY, feedID TEXT NOT NULL, uniqueID TEXT NOT NULL, title TEXT, contentHTML TEXT, contentText TEXT, url TEXT, externalURL TEXT, summary TEXT, imageURL TEXT, bannerImageURL TEXT, datePublished DATE, dateModified DATE, searchRowID INTEGER);
|
||||
|
||||
CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
|
||||
CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
|
||||
|
||||
CREATE TABLE if not EXISTS authors (authorID TEXT NOT NULL PRIMARY KEY, name TEXT, url TEXT, avatarURL TEXT, emailAddress TEXT);
|
||||
CREATE TABLE if not EXISTS authorsLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID));
|
||||
|
||||
@@ -181,7 +181,7 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// 1. Ensure statuses for all the incoming articles.
|
||||
// 2. Create incoming articles with parsedItems.
|
||||
// 3. Ignore incoming articles that are userDeleted
|
||||
// 3. [Deleted - 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.
|
||||
@@ -197,8 +197,7 @@ final class ArticlesTable: DatabaseTable {
|
||||
let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
|
||||
assert(statusesDictionary.count == articleIDs.count)
|
||||
|
||||
let allIncomingArticles = Article.articlesWithParsedItems(parsedItems, webFeedID, self.accountID, statusesDictionary) //2
|
||||
let incomingArticles = Set(allIncomingArticles.filter { !($0.status.userDeleted) }) //3
|
||||
let incomingArticles = Article.articlesWithParsedItems(parsedItems, webFeedID, self.accountID, statusesDictionary) //2
|
||||
if incomingArticles.isEmpty {
|
||||
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
|
||||
return
|
||||
@@ -251,7 +250,7 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
// 1. Ensure statuses for all the incoming articles.
|
||||
// 2. Create incoming articles with parsedItems.
|
||||
// 3. Ignore incoming articles that are userDeleted || (!starred and really old)
|
||||
// 3. Ignore incoming articles that are (!starred and read and really old)
|
||||
// 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.
|
||||
@@ -326,7 +325,7 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0 and userDeleted=0;"
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0;"
|
||||
|
||||
var parameters = [Any]()
|
||||
parameters += Array(webFeedIDs) as [Any]
|
||||
@@ -361,7 +360,7 @@ final class ArticlesTable: DatabaseTable {
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1 and userDeleted=0;"
|
||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1;"
|
||||
let parameters = Array(webFeedIDs) as [Any]
|
||||
|
||||
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
||||
@@ -488,20 +487,41 @@ final class ArticlesTable: DatabaseTable {
|
||||
// MARK: - Cleanup
|
||||
|
||||
/// Delete articles that we won’t show in the UI any longer
|
||||
/// — their arrival date is before our 90-day recency window.
|
||||
/// Keep all starred articles, no matter their age.
|
||||
/// — their arrival date is before our 90-day recency window;
|
||||
/// they are read; they are not starred.
|
||||
///
|
||||
/// 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.
|
||||
func deleteOldArticles() {
|
||||
queue.runInTransaction { databaseResult in
|
||||
precondition(retentionStyle == .syncSystem)
|
||||
|
||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||
let sql = "delete from articles where articleID in (select articleID from articles natural join statuses where dateArrived<? and starred=0);"
|
||||
let parameters = [self.articleCutoffDate] as [Any]
|
||||
queue.runInTransaction { databaseResult in
|
||||
guard let database = databaseResult.database else {
|
||||
return
|
||||
}
|
||||
|
||||
func deleteOldArticles(cutoffDate: Date) {
|
||||
let sql = "delete from articles where articleID in (select articleID from articles natural join statuses where dateArrived<? and read=1 and starred=0);"
|
||||
let parameters = [cutoffDate] as [Any]
|
||||
database.executeUpdate(sql, withArgumentsIn: parameters)
|
||||
}
|
||||
|
||||
if let database = databaseResult.database {
|
||||
makeDatabaseCalls(database)
|
||||
let startTime = Date()
|
||||
func tooMuchTimeHasPassed() -> Bool {
|
||||
let timeElapsed = Date().timeIntervalSince(startTime)
|
||||
return timeElapsed > 2.0
|
||||
}
|
||||
|
||||
let dayIntervals = [365, 300, 225, 150]
|
||||
for dayInterval in dayIntervals {
|
||||
deleteOldArticles(cutoffDate: startTime.bySubtracting(days: dayInterval))
|
||||
if tooMuchTimeHasPassed() {
|
||||
return
|
||||
}
|
||||
}
|
||||
deleteOldArticles(cutoffDate: self.articleCutoffDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,7 +654,7 @@ private extension ArticlesTable {
|
||||
// * 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 userDeleted=0 and (starred=1 or dateArrived>?);"
|
||||
let sql = "select * from articles natural join statuses where \(whereClause) and (starred=1 or dateArrived>?);"
|
||||
return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database)
|
||||
}
|
||||
else {
|
||||
@@ -649,7 +669,7 @@ private extension ArticlesTable {
|
||||
// // * 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 userDeleted=0 and (starred=1 or dateArrived>?);"
|
||||
// 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)
|
||||
// }
|
||||
|
||||
@@ -705,9 +725,6 @@ private extension ArticlesTable {
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
var sql = "select articleID from articles natural join statuses where feedID in \(placeholders) and \(statusKey.rawValue)="
|
||||
sql += value ? "1" : "0"
|
||||
if statusKey != .userDeleted {
|
||||
sql += " and userDeleted=0"
|
||||
}
|
||||
sql += ";"
|
||||
|
||||
let parameters = Array(webFeedIDs) as [Any]
|
||||
@@ -781,18 +798,18 @@ 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 > ?)) and userDeleted = 0"
|
||||
let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))"
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
||||
}
|
||||
|
||||
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred = 1 and userDeleted = 0;
|
||||
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1;
|
||||
if webFeedIDs.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||
let whereClause = "feedID in \(placeholders) and starred = 1 and userDeleted = 0"
|
||||
let whereClause = "feedID in \(placeholders) and starred=1"
|
||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
||||
}
|
||||
|
||||
@@ -931,11 +948,7 @@ private extension ArticlesTable {
|
||||
}
|
||||
|
||||
func articleIsIgnorable(_ article: Article) -> Bool {
|
||||
// Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months).
|
||||
if article.status.userDeleted {
|
||||
return true
|
||||
}
|
||||
if article.status.starred {
|
||||
if article.status.starred || !article.status.read {
|
||||
return false
|
||||
}
|
||||
return article.status.dateArrived < articleCutoffDate
|
||||
|
||||
@@ -42,7 +42,6 @@ struct DatabaseKey {
|
||||
// ArticleStatus
|
||||
static let read = "read"
|
||||
static let starred = "starred"
|
||||
static let userDeleted = "userDeleted"
|
||||
static let dateArrived = "dateArrived"
|
||||
|
||||
// Tag
|
||||
|
||||
@@ -15,9 +15,8 @@ extension ArticleStatus {
|
||||
convenience init(articleID: String, dateArrived: Date, row: FMResultSet) {
|
||||
let read = row.bool(forColumn: DatabaseKey.read)
|
||||
let starred = row.bool(forColumn: DatabaseKey.starred)
|
||||
let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted)
|
||||
|
||||
self.init(articleID: articleID, read: read, starred: starred, userDeleted: userDeleted, dateArrived: dateArrived)
|
||||
|
||||
self.init(articleID: articleID, read: read, starred: starred, dateArrived: dateArrived)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +28,7 @@ extension ArticleStatus: DatabaseObject {
|
||||
}
|
||||
|
||||
public func databaseDictionary() -> DatabaseDictionary? {
|
||||
return [DatabaseKey.articleID: articleID, DatabaseKey.read: read, DatabaseKey.starred: starred, DatabaseKey.userDeleted: userDeleted, DatabaseKey.dateArrived: dateArrived]
|
||||
return [DatabaseKey.articleID: articleID, DatabaseKey.read: read, DatabaseKey.starred: starred, DatabaseKey.dateArrived: dateArrived]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ 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 userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;"
|
||||
let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and (starred=1 or dateArrived>?) group by feedID;"
|
||||
|
||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else {
|
||||
informOperationDelegateOfCompletion()
|
||||
|
||||
@@ -52,7 +52,7 @@ 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 userDeleted=0 and (starred=1 or dateArrived>?);"
|
||||
let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and (starred=1 or dateArrived>?);"
|
||||
|
||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID, cutoffDate]) else {
|
||||
informOperationDelegateOfCompletion()
|
||||
|
||||
@@ -53,7 +53,7 @@ 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 userDeleted=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 and (starred=1 or dateArrived>?) group by feedID;"
|
||||
|
||||
var parameters = [Any]()
|
||||
parameters += Array(webFeedIDs) as [Any]
|
||||
|
||||
@@ -13,7 +13,7 @@ import Articles
|
||||
|
||||
// Article->ArticleStatus is a to-one relationship.
|
||||
//
|
||||
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
|
||||
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
|
||||
|
||||
final class StatusesTable: DatabaseTable {
|
||||
|
||||
@@ -95,11 +95,11 @@ final class StatusesTable: DatabaseTable {
|
||||
// MARK: - Fetching
|
||||
|
||||
func fetchUnreadArticleIDs() throws -> Set<String> {
|
||||
return try fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;")
|
||||
return try fetchArticleIDs("select articleID from statuses where read=0;")
|
||||
}
|
||||
|
||||
func fetchStarredArticleIDs() throws -> Set<String> {
|
||||
return try fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;")
|
||||
return try fetchArticleIDs("select articleID from statuses where starred=1;")
|
||||
}
|
||||
|
||||
func fetchArticleIDsForStatusesWithoutArticlesNewerThan(_ cutoffDate: Date, _ completion: @escaping ArticleIDsCompletionBlock) {
|
||||
@@ -109,7 +109,7 @@ final class StatusesTable: DatabaseTable {
|
||||
var articleIDs = Set<String>()
|
||||
|
||||
func makeDatabaseCall(_ database: FMDatabase) {
|
||||
let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);"
|
||||
let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and not exists (select 1 from articles a where a.articleID = s.articleID);"
|
||||
if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) {
|
||||
articleIDs = resultSet.mapToSet(self.articleIDWithRow)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user