Merge branch 'ios-candidate' of https://github.com/Ranchero-Software/NetNewsWire into ios-candidate
This commit is contained in:
commit
fcd7b88a62
|
@ -235,8 +235,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||||
private enum OperationName {
|
private enum OperationName {
|
||||||
static let FetchAllUnreadCounts = "FetchAllUnreadCounts"
|
static let FetchAllUnreadCounts = "FetchAllUnreadCounts"
|
||||||
static let FetchFeedUnreadCount = "FetchFeedUnreadCount"
|
static let FetchFeedUnreadCount = "FetchFeedUnreadCount"
|
||||||
|
static let FetchUnreadCountsForFeeds = "FetchUnreadCountsForFeeds"
|
||||||
}
|
}
|
||||||
private static let discardableOperationNames = [OperationName.FetchAllUnreadCounts, OperationName.FetchFeedUnreadCount]
|
private static let discardableOperationNames = [OperationName.FetchAllUnreadCounts, OperationName.FetchFeedUnreadCount, OperationName.FetchUnreadCountsForFeeds]
|
||||||
|
|
||||||
init?(dataFolder: String, type: AccountType, accountID: String, transport: Transport? = nil) {
|
init?(dataFolder: String, type: AccountType, accountID: String, transport: Transport? = nil) {
|
||||||
switch type {
|
switch type {
|
||||||
|
@ -622,27 +623,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateUnreadCounts(for webFeeds: Set<WebFeed>, completion: VoidCompletionBlock? = nil) {
|
public func updateUnreadCounts(for webFeeds: Set<WebFeed>, completion: VoidCompletionBlock? = nil) {
|
||||||
if webFeeds.isEmpty {
|
fetchUnreadCounts(for: webFeeds, completion: completion)
|
||||||
completion?()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if webFeeds.count == 1, let feed = webFeeds.first {
|
|
||||||
fetchUnreadCount(feed, completion)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
database.fetchUnreadCounts(for: webFeeds.webFeedIDs()) { unreadCountDictionaryResult in
|
|
||||||
if let unreadCountDictionary = try? unreadCountDictionaryResult.get() {
|
|
||||||
for webFeed in webFeeds {
|
|
||||||
if let unreadCount = unreadCountDictionary[webFeed.webFeedID] {
|
|
||||||
webFeed.unreadCount = unreadCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> {
|
public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> {
|
||||||
|
@ -1246,6 +1227,25 @@ private extension Account {
|
||||||
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.articleIDs: articleIDs])
|
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.articleIDs: articleIDs])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch unread counts for zero or more feeds.
|
||||||
|
///
|
||||||
|
/// Uses the most efficient method based on how many feeds were passed in.
|
||||||
|
func fetchUnreadCounts(for feeds: Set<WebFeed>, completion: VoidCompletionBlock?) {
|
||||||
|
if feeds.isEmpty {
|
||||||
|
completion?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if feeds.count == 1, let feed = feeds.first {
|
||||||
|
fetchUnreadCount(feed, completion)
|
||||||
|
}
|
||||||
|
else if feeds.count < 10 {
|
||||||
|
fetchUnreadCounts(feeds, completion)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fetchAllUnreadCounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fetchUnreadCount(_ feed: WebFeed, _ completion: VoidCompletionBlock?) {
|
func fetchUnreadCount(_ feed: WebFeed, _ completion: VoidCompletionBlock?) {
|
||||||
let operation = database.createFetchFeedUnreadCountOperation(feedID: feed.webFeedID)
|
let operation = database.createFetchFeedUnreadCountOperation(feedID: feed.webFeedID)
|
||||||
operation.name = OperationName.FetchFeedUnreadCount
|
operation.name = OperationName.FetchFeedUnreadCount
|
||||||
|
@ -1260,6 +1260,21 @@ private extension Account {
|
||||||
operationQueue.addOperation(operation)
|
operationQueue.addOperation(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchUnreadCounts(_ feeds: Set<WebFeed>, _ completion: VoidCompletionBlock?) {
|
||||||
|
let feedIDs = feeds.map { $0.webFeedID }
|
||||||
|
let operation = database.createFetchUnreadCountsForFeedsOperation(feedIDs: Set(feedIDs))
|
||||||
|
operation.name = OperationName.FetchUnreadCountsForFeeds
|
||||||
|
operation.completionBlock = { operation in
|
||||||
|
let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation
|
||||||
|
if let unreadCountDictionary = fetchOperation.unreadCountDictionary {
|
||||||
|
self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: feeds)
|
||||||
|
}
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
|
||||||
|
operationQueue.addOperation(operation)
|
||||||
|
}
|
||||||
|
|
||||||
func fetchAllUnreadCounts() {
|
func fetchAllUnreadCounts() {
|
||||||
fetchingAllUnreadCounts = true
|
fetchingAllUnreadCounts = true
|
||||||
operationQueue.cancelOperations(named: OperationName.FetchAllUnreadCounts)
|
operationQueue.cancelOperations(named: OperationName.FetchAllUnreadCounts)
|
||||||
|
@ -1271,7 +1286,7 @@ private extension Account {
|
||||||
guard let unreadCountDictionary = fetchOperation.unreadCountDictionary else {
|
guard let unreadCountDictionary = fetchOperation.unreadCountDictionary else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary)
|
self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: self.flattenedWebFeeds())
|
||||||
|
|
||||||
self.fetchingAllUnreadCounts = false
|
self.fetchingAllUnreadCounts = false
|
||||||
self.updateUnreadCount()
|
self.updateUnreadCount()
|
||||||
|
@ -1282,8 +1297,8 @@ private extension Account {
|
||||||
operationQueue.addOperation(operation)
|
operationQueue.addOperation(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary) {
|
func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set<WebFeed>) {
|
||||||
for feed in flattenedWebFeeds() {
|
for feed in feeds {
|
||||||
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
||||||
let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0
|
let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0
|
||||||
feed.unreadCount = unreadCount
|
feed.unreadCount = unreadCount
|
||||||
|
|
|
@ -80,4 +80,18 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Hashable
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .smartFeed(let id):
|
||||||
|
hasher.combine(id)
|
||||||
|
case .script(let id):
|
||||||
|
hasher.combine(id)
|
||||||
|
case .webFeed(_, let webFeedID):
|
||||||
|
hasher.combine(webFeedID)
|
||||||
|
case .folder(_, let folderName):
|
||||||
|
hasher.combine(folderName)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
||||||
operationQueue.addOperation(addRequest)
|
operationQueue.addOperation(addRequest)
|
||||||
|
|
||||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log)
|
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log)
|
||||||
|
createFeeds.delegate = self
|
||||||
createFeeds.addDependency(addRequest)
|
createFeeds.addDependency(addRequest)
|
||||||
createFeeds.downloadProgress = downloadProgress
|
createFeeds.downloadProgress = downloadProgress
|
||||||
operationQueue.addOperation(createFeeds)
|
operationQueue.addOperation(createFeeds)
|
||||||
|
@ -91,17 +92,20 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
||||||
let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, database: database, newerThan: nil, log: log)
|
let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, database: database, newerThan: nil, log: log)
|
||||||
syncUnread.addDependency(createFeeds)
|
syncUnread.addDependency(createFeeds)
|
||||||
syncUnread.downloadProgress = downloadProgress
|
syncUnread.downloadProgress = downloadProgress
|
||||||
|
syncUnread.delegate = self
|
||||||
operationQueue.addOperation(syncUnread)
|
operationQueue.addOperation(syncUnread)
|
||||||
|
|
||||||
let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: feedResourceId, service: getStreamContentsService, isPagingEnabled: false, newerThan: nil, log: log)
|
let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: feedResourceId, service: getStreamContentsService, isPagingEnabled: false, newerThan: nil, log: log)
|
||||||
syncFeed.addDependency(syncUnread)
|
syncFeed.addDependency(syncUnread)
|
||||||
syncFeed.downloadProgress = downloadProgress
|
syncFeed.downloadProgress = downloadProgress
|
||||||
|
syncFeed.delegate = self
|
||||||
operationQueue.addOperation(syncFeed)
|
operationQueue.addOperation(syncFeed)
|
||||||
|
|
||||||
let finishOperation = FeedlyCheckpointOperation()
|
let finishOperation = FeedlyCheckpointOperation()
|
||||||
finishOperation.checkpointDelegate = self
|
finishOperation.checkpointDelegate = self
|
||||||
finishOperation.downloadProgress = downloadProgress
|
finishOperation.downloadProgress = downloadProgress
|
||||||
finishOperation.addDependency(syncFeed)
|
finishOperation.addDependency(syncFeed)
|
||||||
|
finishOperation.delegate = self
|
||||||
operationQueue.addOperation(finishOperation)
|
operationQueue.addOperation(finishOperation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +113,8 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
||||||
addCompletionHandler?(.failure(error))
|
addCompletionHandler?(.failure(error))
|
||||||
addCompletionHandler = nil
|
addCompletionHandler = nil
|
||||||
|
|
||||||
|
os_log(.debug, log: log, "Unable to add new feed: %{public}@.", error as NSError)
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,10 +137,6 @@ public final class ArticlesDatabase {
|
||||||
|
|
||||||
// MARK: - Unread Counts
|
// MARK: - Unread Counts
|
||||||
|
|
||||||
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
|
||||||
articlesTable.fetchUnreadCounts(webFeedIDs, completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func fetchUnreadCountForToday(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
public func fetchUnreadCountForToday(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||||
fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion)
|
fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion)
|
||||||
}
|
}
|
||||||
|
@ -203,6 +199,11 @@ public final class ArticlesDatabase {
|
||||||
return FetchFeedUnreadCountOperation(feedID: feedID, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
return FetchFeedUnreadCountOperation(feedID: feedID, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create an operation that fetches unread counts for a number of feedIDs.
|
||||||
|
public func createFetchUnreadCountsForFeedsOperation(feedIDs: Set<String>) -> FetchUnreadCountsForFeedsOperation {
|
||||||
|
return FetchUnreadCountsForFeedsOperation(feedIDs: feedIDs, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Suspend and Resume (for iOS)
|
// MARK: - Suspend and Resume (for iOS)
|
||||||
|
|
||||||
/// Close the database and stop running database calls.
|
/// Close the database and stop running database calls.
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; };
|
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; };
|
||||||
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; };
|
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; };
|
||||||
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; };
|
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; };
|
||||||
|
84611DCC23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */; };
|
||||||
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */; };
|
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */; };
|
||||||
848E3EB920FBCFD20004B7ED /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EB820FBCFD20004B7ED /* RSCore.framework */; };
|
848E3EB920FBCFD20004B7ED /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EB820FBCFD20004B7ED /* RSCore.framework */; };
|
||||||
848E3EBD20FBCFDE0004B7ED /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */; };
|
848E3EBD20FBCFDE0004B7ED /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */; };
|
||||||
|
@ -126,6 +127,7 @@
|
||||||
845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||||
845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = "<group>"; };
|
845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = "<group>"; };
|
||||||
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = "<group>"; };
|
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = "<group>"; };
|
||||||
|
84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUnreadCountsForFeedsOperation.swift; sourceTree = "<group>"; };
|
||||||
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
|
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
|
||||||
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTable.swift; sourceTree = "<group>"; };
|
8477ACBB2221E76F00DF7F37 /* SearchTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTable.swift; sourceTree = "<group>"; };
|
||||||
848E3EB820FBCFD20004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
848E3EB820FBCFD20004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -245,8 +247,9 @@
|
||||||
84C242C723DEB42700C50516 /* Operations */ = {
|
84C242C723DEB42700C50516 /* Operations */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */,
|
|
||||||
84116B8823E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift */,
|
84116B8823E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift */,
|
||||||
|
84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */,
|
||||||
|
84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */,
|
||||||
);
|
);
|
||||||
path = Operations;
|
path = Operations;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -535,6 +538,7 @@
|
||||||
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
|
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
|
||||||
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */,
|
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */,
|
||||||
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */,
|
8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */,
|
||||||
|
84611DCC23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift in Sources */,
|
||||||
843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */,
|
843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */,
|
||||||
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */,
|
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */,
|
||||||
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */,
|
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */,
|
||||||
|
|
|
@ -237,31 +237,6 @@ final class ArticlesTable: DatabaseTable {
|
||||||
|
|
||||||
// MARK: - Unread Counts
|
// MARK: - Unread Counts
|
||||||
|
|
||||||
func fetchUnreadCounts(_ webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
|
||||||
if webFeedIDs.isEmpty {
|
|
||||||
completion(.success(UnreadCountDictionary()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAllUnreadCounts { (unreadCountsResult) in
|
|
||||||
|
|
||||||
func createUnreadCountDictionary(_ unreadCountDictionary: UnreadCountDictionary) -> UnreadCountDictionary {
|
|
||||||
var d = UnreadCountDictionary()
|
|
||||||
for webFeedID in webFeedIDs {
|
|
||||||
d[webFeedID] = unreadCountDictionary[webFeedID] ?? 0
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
switch unreadCountsResult {
|
|
||||||
case .success(let unreadCountDictionary):
|
|
||||||
completion(.success(createUnreadCountDictionary(unreadCountDictionary)))
|
|
||||||
case .failure(let databaseError):
|
|
||||||
completion(.failure(databaseError))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchUnreadCount(_ webFeedIDs: Set<String>, _ since: Date, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
func fetchUnreadCount(_ webFeedIDs: Set<String>, _ since: Date, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||||
// Get unread count for today, for instance.
|
// Get unread count for today, for instance.
|
||||||
if webFeedIDs.isEmpty {
|
if webFeedIDs.isEmpty {
|
||||||
|
@ -298,46 +273,6 @@ final class ArticlesTable: DatabaseTable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
|
||||||
// Returns only where unreadCount > 0.
|
|
||||||
|
|
||||||
let cutoffDate = articleCutoffDate
|
|
||||||
queue.runInDatabase { databaseResult in
|
|
||||||
|
|
||||||
func makeDatabaseCalls(_ 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;"
|
|
||||||
|
|
||||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.success(UnreadCountDictionary()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = UnreadCountDictionary()
|
|
||||||
while resultSet.next() {
|
|
||||||
let unreadCount = resultSet.long(forColumnIndex: 1)
|
|
||||||
if let webFeedID = resultSet.string(forColumnIndex: 0) {
|
|
||||||
d[webFeedID] = unreadCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.success(d))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch databaseResult {
|
|
||||||
case .success(let database):
|
|
||||||
makeDatabaseCalls(database)
|
|
||||||
case .failure(let databaseError):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(databaseError))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchStarredAndUnreadCount(_ webFeedIDs: Set<String>, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
func fetchStarredAndUnreadCount(_ webFeedIDs: Set<String>, _ completion: @escaping SingleUnreadCountCompletionBlock) {
|
||||||
if webFeedIDs.isEmpty {
|
if webFeedIDs.isEmpty {
|
||||||
completion(.success(0))
|
completion(.success(0))
|
||||||
|
@ -630,15 +565,15 @@ private extension ArticlesTable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadCount(_ webFeedID: String, _ database: FMDatabase) -> Int {
|
// func fetchUnreadCount(_ webFeedID: String, _ database: FMDatabase) -> Int {
|
||||||
// Count only the articles that would appear in the UI.
|
// // Count only the articles that would appear in the UI.
|
||||||
// * Must be unread.
|
// // * Must be unread.
|
||||||
// * Must not be deleted.
|
// // * Must not be deleted.
|
||||||
// * Must be either 1) starred or 2) dateArrived must be newer than cutoff date.
|
// // * 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 userDeleted=0 and (starred=1 or dateArrived>?);"
|
||||||
return numberWithSQLAndParameters(sql, [webFeedID, articleCutoffDate], in: database)
|
// return numberWithSQLAndParameters(sql, [webFeedID, articleCutoffDate], in: database)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> {
|
func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> {
|
||||||
let sql = "select rowid from search where search match ?;"
|
let sql = "select rowid from search where search match ?;"
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
//
|
||||||
|
// FetchUnreadCountsForFeedsOperation.swift
|
||||||
|
// ArticlesDatabase
|
||||||
|
//
|
||||||
|
// Created by Brent Simmons on 2/1/20.
|
||||||
|
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import RSCore
|
||||||
|
import RSDatabase
|
||||||
|
|
||||||
|
/// Fetch the unread counts for a number of feeds.
|
||||||
|
public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation {
|
||||||
|
|
||||||
|
public var unreadCountDictionary: UnreadCountDictionary?
|
||||||
|
public let feedIDs: Set<String>
|
||||||
|
|
||||||
|
// MainThreadOperation
|
||||||
|
public var isCanceled = false
|
||||||
|
public var id: Int?
|
||||||
|
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||||
|
public var name: String?
|
||||||
|
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||||
|
|
||||||
|
private let queue: DatabaseQueue
|
||||||
|
private let cutoffDate: Date
|
||||||
|
|
||||||
|
init(feedIDs: Set<String>, databaseQueue: DatabaseQueue, cutoffDate: Date) {
|
||||||
|
self.feedIDs = feedIDs
|
||||||
|
self.queue = databaseQueue
|
||||||
|
self.cutoffDate = cutoffDate
|
||||||
|
}
|
||||||
|
|
||||||
|
public func run() {
|
||||||
|
queue.runInDatabase { databaseResult in
|
||||||
|
if self.isCanceled {
|
||||||
|
self.informOperationDelegateOfCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch databaseResult {
|
||||||
|
case .success(let database):
|
||||||
|
self.fetchUnreadCounts(database)
|
||||||
|
case .failure:
|
||||||
|
self.informOperationDelegateOfCompletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FetchUnreadCountsForFeedsOperation {
|
||||||
|
|
||||||
|
func fetchUnreadCounts(_ database: FMDatabase) {
|
||||||
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.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;"
|
||||||
|
|
||||||
|
var parameters = [Any]()
|
||||||
|
parameters += Array(feedIDs) as [Any]
|
||||||
|
parameters += [cutoffDate] as [Any]
|
||||||
|
|
||||||
|
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
||||||
|
informOperationDelegateOfCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isCanceled {
|
||||||
|
informOperationDelegateOfCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = UnreadCountDictionary()
|
||||||
|
while resultSet.next() {
|
||||||
|
if isCanceled {
|
||||||
|
resultSet.close()
|
||||||
|
informOperationDelegateOfCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let unreadCount = resultSet.long(forColumnIndex: 1)
|
||||||
|
if let webFeedID = resultSet.string(forColumnIndex: 0) {
|
||||||
|
d[webFeedID] = unreadCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close()
|
||||||
|
|
||||||
|
unreadCountDictionary = d
|
||||||
|
informOperationDelegateOfCompletion()
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ final class FaviconDownloader {
|
||||||
private let diskCache: BinaryDiskCache
|
private let diskCache: BinaryDiskCache
|
||||||
private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader
|
private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader
|
||||||
private var remainingFaviconURLs = [String: ArraySlice<String>]() // homePageURL: array of faviconURLs that haven't been checked yet
|
private var remainingFaviconURLs = [String: ArraySlice<String>]() // homePageURL: array of faviconURLs that haven't been checked yet
|
||||||
|
private var currentHomePageHasOnlyFaviconICO = false
|
||||||
|
|
||||||
private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL
|
private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL
|
||||||
private var homePageToFaviconURLCachePath: String
|
private var homePageToFaviconURLCachePath: String
|
||||||
|
@ -131,20 +132,14 @@ final class FaviconDownloader {
|
||||||
}
|
}
|
||||||
|
|
||||||
findFaviconURLs(with: url) { (faviconURLs) in
|
findFaviconURLs(with: url) { (faviconURLs) in
|
||||||
var hasIcons = false
|
|
||||||
|
|
||||||
if let faviconURLs = faviconURLs {
|
if let faviconURLs = faviconURLs {
|
||||||
|
self.currentHomePageHasOnlyFaviconICO = faviconURLs.count == 1
|
||||||
|
|
||||||
if let firstIconURL = faviconURLs.first {
|
if let firstIconURL = faviconURLs.first {
|
||||||
hasIcons = true
|
|
||||||
let _ = self.favicon(with: firstIconURL, homePageURL: url)
|
let _ = self.favicon(with: firstIconURL, homePageURL: url)
|
||||||
self.remainingFaviconURLs[url] = faviconURLs.dropFirst()
|
self.remainingFaviconURLs[url] = faviconURLs.dropFirst()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasIcons) {
|
|
||||||
self.homePageURLsWithNoFaviconURLCache.insert(url)
|
|
||||||
self.homePageURLsWithNoFaviconURLCacheDirty = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -167,6 +162,11 @@ final class FaviconDownloader {
|
||||||
remainingFaviconURLs[homePageURL] = faviconURLs.dropFirst();
|
remainingFaviconURLs[homePageURL] = faviconURLs.dropFirst();
|
||||||
} else {
|
} else {
|
||||||
remainingFaviconURLs[homePageURL] = nil
|
remainingFaviconURLs[homePageURL] = nil
|
||||||
|
|
||||||
|
if currentHomePageHasOnlyFaviconICO {
|
||||||
|
self.homePageURLsWithNoFaviconURLCache.insert(homePageURL)
|
||||||
|
self.homePageURLsWithNoFaviconURLCacheDirty = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
@ -215,7 +215,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||||
headerView.addGestureRecognizer(tap)
|
headerView.addGestureRecognizer(tap)
|
||||||
|
|
||||||
// Without this the swipe gesture registers on the cell below
|
// Without this the swipe gesture registers on the cell below
|
||||||
headerView.addGestureRecognizer(UIPanGestureRecognizer(target: nil, action: nil))
|
let gestureRecognizer = UIPanGestureRecognizer(target: nil, action: nil)
|
||||||
|
gestureRecognizer.delegate = self
|
||||||
|
headerView.addGestureRecognizer(gestureRecognizer)
|
||||||
|
|
||||||
headerView.interactions.removeAll()
|
headerView.interactions.removeAll()
|
||||||
if section != 0 {
|
if section != 0 {
|
||||||
|
@ -1229,3 +1231,13 @@ private extension MasterFeedViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MasterFeedViewController: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let velocity = gestureRecognizer.velocity(in: self.view)
|
||||||
|
return abs(velocity.x) > abs(velocity.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ class RefreshProgressView: UIView {
|
||||||
|
|
||||||
@IBOutlet weak var progressView: UIProgressView!
|
@IBOutlet weak var progressView: UIProgressView!
|
||||||
@IBOutlet weak var label: UILabel!
|
@IBOutlet weak var label: UILabel!
|
||||||
private lazy var progressWidth = progressView.widthAnchor.constraint(equalToConstant: 100.0)
|
private lazy var progressWidthConstraint = progressView.widthAnchor.constraint(equalToConstant: 100.0)
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||||
|
@ -28,6 +28,10 @@ class RefreshProgressView: UIView {
|
||||||
scheduleUpdateRefreshLabel()
|
scheduleUpdateRefreshLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didMoveToSuperview() {
|
||||||
|
progressChanged()
|
||||||
|
}
|
||||||
|
|
||||||
func updateRefreshLabel() {
|
func updateRefreshLabel() {
|
||||||
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
||||||
|
|
||||||
|
@ -71,23 +75,32 @@ class RefreshProgressView: UIView {
|
||||||
private extension RefreshProgressView {
|
private extension RefreshProgressView {
|
||||||
|
|
||||||
func progressChanged() {
|
func progressChanged() {
|
||||||
|
// Layout may crash if not in the view hierarchy.
|
||||||
|
// https://github.com/Ranchero-Software/NetNewsWire/issues/1764
|
||||||
|
let isInViewHierarchy = self.superview != nil
|
||||||
|
|
||||||
let progress = AccountManager.shared.combinedRefreshProgress
|
let progress = AccountManager.shared.combinedRefreshProgress
|
||||||
|
|
||||||
if progress.isComplete {
|
if progress.isComplete {
|
||||||
|
if isInViewHierarchy {
|
||||||
progressView.setProgress(1, animated: true)
|
progressView.setProgress(1, animated: true)
|
||||||
|
}
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
self.updateRefreshLabel()
|
self.updateRefreshLabel()
|
||||||
self.label.isHidden = false
|
self.label.isHidden = false
|
||||||
self.progressView.isHidden = true
|
self.progressView.isHidden = true
|
||||||
self.progressWidth.isActive = false
|
self.progressWidthConstraint.isActive = false
|
||||||
|
if isInViewHierarchy {
|
||||||
self.progressView.setProgress(0, animated: true)
|
self.progressView.setProgress(0, animated: true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
label.isHidden = true
|
label.isHidden = true
|
||||||
progressView.isHidden = false
|
progressView.isHidden = false
|
||||||
self.progressWidth.isActive = true
|
progressWidthConstraint.isActive = true
|
||||||
self.progressView.setNeedsLayout()
|
if isInViewHierarchy {
|
||||||
self.progressView.layoutIfNeeded()
|
progressView.setNeedsLayout()
|
||||||
|
progressView.layoutIfNeeded()
|
||||||
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
|
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
|
||||||
|
|
||||||
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
// Don't let the progress bar go backwards unless we need to go back more than 25%
|
||||||
|
@ -96,6 +109,7 @@ private extension RefreshProgressView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func scheduleUpdateRefreshLabel() {
|
func scheduleUpdateRefreshLabel() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
||||||
MARKETING_VERSION = 5.0
|
MARKETING_VERSION = 5.0
|
||||||
CURRENT_PROJECT_VERSION = 33
|
CURRENT_PROJECT_VERSION = 34
|
||||||
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||||
|
|
Loading…
Reference in New Issue