Merge branch 'master' into extension-point

This commit is contained in:
Maurice Parker 2020-04-11 18:14:30 -05:00
commit a131c01e77
18 changed files with 315 additions and 119 deletions

View File

@ -716,7 +716,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
webFeedDictionariesNeedUpdate = true
}
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping DatabaseCompletionBlock) {
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) {
// Used only by an On My Mac or iCloud account.
precondition(Thread.isMainThread)
precondition(type == .onMyMac || type == .cloudKit)
@ -724,14 +724,14 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
webFeed.takeSettings(from: parsedFeed)
let parsedItems = parsedFeed.items
guard !parsedItems.isEmpty else {
completion(nil)
completion(.success(NewAndUpdatedArticles()))
return
}
update(webFeed.webFeedID, with: parsedItems, completion: completion)
}
func update(_ webFeedID: String, with parsedItems: Set<ParsedItem>, completion: @escaping DatabaseCompletionBlock) {
func update(_ webFeedID: String, with parsedItems: Set<ParsedItem>, completion: @escaping UpdateArticlesCompletionBlock) {
// Used only by an On My Mac or iCloud account.
precondition(Thread.isMainThread)
precondition(type == .onMyMac || type == .cloudKit)
@ -740,9 +740,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
switch updateArticlesResult {
case .success(let newAndUpdatedArticles):
self.sendNotificationAbout(newAndUpdatedArticles)
completion(nil)
completion(.success(newAndUpdatedArticles))
case .failure(let databaseError):
completion(databaseError)
completion(.failure(databaseError))
}
}
}
@ -801,39 +801,45 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
/// Mark articleIDs statuses based on statusKey and flag.
/// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
/// Returns a set of new article statuses.
func markAndFetchNew(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: ArticleIDsCompletionBlock? = nil) {
guard !articleIDs.isEmpty else {
completion?(nil)
completion?(.success(Set<String>()))
return
}
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
if let error = error {
completion?(error)
return
database.markAndFetchNew(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { result in
switch result {
case .success(let newArticleStatusIDs):
self.noteStatusesForArticleIDsDidChange(articleIDs)
completion?(.success(newArticleStatusIDs))
case .failure(let databaseError):
completion?(.failure(databaseError))
}
self.noteStatusesForArticleIDsDidChange(articleIDs)
completion?(nil)
}
}
/// Mark articleIDs as read. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsRead(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
mark(articleIDs: articleIDs, statusKey: .read, flag: true, completion: completion)
/// Returns a set of new article statuses.
func markAsRead(_ articleIDs: Set<String>, completion: ArticleIDsCompletionBlock? = nil) {
markAndFetchNew(articleIDs: articleIDs, statusKey: .read, flag: true, completion: completion)
}
/// Mark articleIDs as unread. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsUnread(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
mark(articleIDs: articleIDs, statusKey: .read, flag: false, completion: completion)
/// Returns a set of new article statuses.
func markAsUnread(_ articleIDs: Set<String>, completion: ArticleIDsCompletionBlock? = nil) {
markAndFetchNew(articleIDs: articleIDs, statusKey: .read, flag: false, completion: completion)
}
/// Mark articleIDs as starred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsStarred(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
mark(articleIDs: articleIDs, statusKey: .starred, flag: true, completion: completion)
/// Returns a set of new article statuses.
func markAsStarred(_ articleIDs: Set<String>, completion: ArticleIDsCompletionBlock? = nil) {
markAndFetchNew(articleIDs: articleIDs, statusKey: .starred, flag: true, completion: completion)
}
/// Mark articleIDs as unstarred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
func markAsUnstarred(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
mark(articleIDs: articleIDs, statusKey: .starred, flag: false, completion: completion)
/// Returns a set of new article statuses.
func markAsUnstarred(_ articleIDs: Set<String>, completion: ArticleIDsCompletionBlock? = nil) {
markAndFetchNew(articleIDs: articleIDs, statusKey: .starred, flag: false, completion: completion)
}
/// Empty caches that can reasonably be emptied. Call when the app goes in the background, for instance.
@ -888,7 +894,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func debugDropConditionalGetInfo() {
#if DEBUG
flattenedWebFeeds().forEach{ $0.debugDropConditionalGetInfo() }
flattenedWebFeeds().forEach{ $0.dropConditionalGetInfo() }
#endif
}

View File

@ -8,11 +8,13 @@
import Foundation
import CloudKit
import SystemConfiguration
import os.log
import SyncDatabase
import RSCore
import RSParser
import Articles
import ArticlesDatabase
import RSWeb
import Secrets
@ -35,7 +37,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
private let accountZone: CloudKitAccountZone
private let articlesZone: CloudKitArticlesZone
private let refresher = LocalAccountRefresher()
weak var account: Account?
private lazy var refresher: LocalAccountRefresher = {
let refresher = LocalAccountRefresher()
refresher.delegate = self
return refresher
}()
let behaviors: AccountBehaviors = []
let isOPMLImportInProgress = false
@ -45,7 +53,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
var accountMetadata: AccountMetadata?
var refreshProgress = DownloadProgress(numberOfTasks: 0)
init(dataFolder: String) {
accountZone = CloudKitAccountZone(container: container)
articlesZone = CloudKitArticlesZone(container: container)
@ -77,6 +85,14 @@ final class CloudKitAccountDelegate: AccountDelegate {
completion(.success(()))
return
}
let reachability = SCNetworkReachabilityCreateWithName(nil, "apple.com")
var flags = SCNetworkReachabilityFlags()
guard SCNetworkReachabilityGetFlags(reachability!, &flags), flags.contains(.reachable) else {
completion(.success(()))
return
}
refreshAll(for: account, downloadFeeds: true, completion: completion)
}
@ -91,12 +107,12 @@ final class CloudKitAccountDelegate: AccountDelegate {
return
}
let starredArticleIDs = syncStatuses.filter({ $0.key == .starred && $0.flag == true }).map({ $0.articleID })
account.fetchArticlesAsync(.articleIDs(Set(starredArticleIDs))) { result in
let articleIDs = syncStatuses.map({ $0.articleID })
account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in
func processWithArticles(_ starredArticles: Set<Article>) {
func processWithArticles(_ articles: Set<Article>) {
self.articlesZone.sendArticleStatus(syncStatuses, starredArticles: starredArticles) { result in
self.articlesZone.sendArticleStatus(syncStatuses, articles: articles) { result in
switch result {
case .success:
self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID }) )
@ -112,8 +128,8 @@ final class CloudKitAccountDelegate: AccountDelegate {
}
switch result {
case .success(let starredArticles):
processWithArticles(starredArticles)
case .success(let articles):
processWithArticles(articles)
case .failure(let databaseError):
completion(.failure(databaseError))
}
@ -147,6 +163,11 @@ final class CloudKitAccountDelegate: AccountDelegate {
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
guard refreshProgress.isComplete else {
completion(.success(()))
return
}
var fileData: Data?
do {
@ -464,8 +485,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
}
func accountDidInitialize(_ account: Account) {
self.account = account
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress)
articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account, database: database, articlesZone: articlesZone)
articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account,
database: database,
articlesZone: articlesZone,
refreshProgress: refreshProgress)
// Check to see if this is a new account and initialize anything we need
if account.externalID == nil {
@ -473,11 +499,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
switch result {
case .success(let externalID):
account.externalID = externalID
self.refreshAll(for: account, downloadFeeds: false) { result in
if case .failure(let error) = result {
os_log(.error, log: self.log, "Error while doing intial refresh: %@", error.localizedDescription)
}
}
self.refreshAll(for: account, downloadFeeds: false) { _ in }
case .failure(let error):
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
}
@ -522,6 +544,12 @@ private extension CloudKitAccountDelegate {
let intialWebFeedsCount = downloadFeeds ? account.flattenedWebFeeds().count : 0
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
func fail(_ error: Error) {
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
}
BatchUpdate.shared.start()
accountZone.fetchChangesInZone() { result in
BatchUpdate.shared.end()
@ -550,31 +578,34 @@ private extension CloudKitAccountDelegate {
return
}
self.refresher.refreshFeeds(webFeeds, feedCompletionBlock: { _ in self.refreshProgress.completeTask() }) {
self.refresher.refreshFeeds(webFeeds) {
account.metadata.lastArticleFetchEndTime = Date()
self.refreshProgress.clear()
completion(.success(()))
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
fail(error)
}
}
}
case .failure(let error):
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
fail(error)
}
}
case .failure(let error):
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
fail(error)
}
}
case .failure(let error):
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
fail(error)
}
}
}
@ -589,3 +620,24 @@ private extension CloudKitAccountDelegate {
}
}
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, didProcess newAndUpdatedArticles: NewAndUpdatedArticles) {
if let newArticles = newAndUpdatedArticles.newArticles {
let syncStatuses = newArticles.map { article in
return SyncStatus(articleID: article.articleID, key: .read, flag: false)
}
database.insertStatuses(syncStatuses)
}
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
refreshProgress.completeTask()
}
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
refreshProgress.clear()
}
}

View File

@ -48,6 +48,7 @@ final class CloudKitArticlesZone: CloudKitZone {
struct CloudKitArticleStatus {
static let recordType = "ArticleStatus"
struct Fields {
static let webFeedExternalID = "webFeedExternalID"
static let read = "read"
static let starred = "starred"
static let userDeleted = "userDeleted"
@ -81,8 +82,11 @@ final class CloudKitArticlesZone: CloudKitZone {
}
}
func sendArticleStatus(_ syncStatuses: [SyncStatus], starredArticles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
var records = makeStatusRecords(syncStatuses)
func sendArticleStatus(_ syncStatuses: [SyncStatus], articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
var records = makeStatusRecords(syncStatuses, articles)
let starredArticles = articles.filter({ $0.status.starred == true })
makeArticleRecordsIfNecessary(starredArticles) { result in
switch result {
case .success(let articleRecords):
@ -92,11 +96,11 @@ final class CloudKitArticlesZone: CloudKitZone {
case .success:
completion(.success(()))
case .failure(let error):
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: starredArticles, completion: completion)
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: articles, completion: completion)
}
}
case .failure(let error):
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: starredArticles, completion: completion)
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: articles, completion: completion)
}
}
}
@ -106,7 +110,7 @@ final class CloudKitArticlesZone: CloudKitZone {
self.createZoneRecord() { result in
switch result {
case .success:
self.sendArticleStatus(syncStatuses, starredArticles: starredArticles, completion: completion)
self.sendArticleStatus(syncStatuses, articles: starredArticles, completion: completion)
case .failure(let error):
completion(.failure(error))
}
@ -120,7 +124,13 @@ final class CloudKitArticlesZone: CloudKitZone {
private extension CloudKitArticlesZone {
func makeStatusRecords(_ syncStatuses: [SyncStatus]) -> [CKRecord] {
func makeStatusRecords(_ syncStatuses: [SyncStatus], _ articles: Set<Article>) -> [CKRecord] {
var articleDict = [String: Article]()
for article in articles {
articleDict[article.articleID] = article
}
var records = [String: CKRecord]()
for status in syncStatuses {
@ -132,6 +142,10 @@ private extension CloudKitArticlesZone {
records[status.articleID] = record
}
if let webFeedExternalID = articleDict[status.articleID]?.webFeed?.externalID {
record![CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
}
switch status.key {
case .read:
record![CloudKitArticleStatus.Fields.read] = status.flag ? "1" : "0"

View File

@ -9,8 +9,11 @@
import Foundation
import os.log
import RSParser
import RSWeb
import CloudKit
import SyncDatabase
import Articles
import ArticlesDatabase
class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
@ -19,11 +22,19 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
weak var account: Account?
var database: SyncDatabase
weak var articlesZone: CloudKitArticlesZone?
weak var refreshProgress: DownloadProgress?
private lazy var refresher: LocalAccountRefresher = {
let refresher = LocalAccountRefresher()
refresher.delegate = self
return refresher
}()
init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone) {
init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone, refreshProgress: DownloadProgress?) {
self.account = account
self.database = database
self.articlesZone = articlesZone
self.refreshProgress = refreshProgress
}
func cloudKitDidChange(record: CKRecord) {
@ -82,8 +93,40 @@ private extension CloudKitArticlesZoneDelegate {
let group = DispatchGroup()
group.enter()
account?.markAsUnread(updateableUnreadArticleIDs) { _ in
group.leave()
account?.markAsUnread(updateableUnreadArticleIDs) { result in
switch result {
case .success(let newArticleStatusIDs):
if newArticleStatusIDs.isEmpty {
group.leave()
} else {
var webFeedExternalIDDict = [String: String]()
for record in records {
if let webFeedExternalID = record[CloudKitArticlesZone.CloudKitArticleStatus.Fields.webFeedExternalID] as? String {
webFeedExternalIDDict[record.externalID] = webFeedExternalID
}
}
var webFeeds = Set<WebFeed>()
for newArticleStatusID in newArticleStatusIDs {
if let webFeedExternalID = webFeedExternalIDDict[newArticleStatusID],
let webFeed = self.account?.existingWebFeed(withExternalID: webFeedExternalID) {
webFeeds.insert(webFeed)
}
}
webFeeds.forEach { $0.dropConditionalGetInfo() }
self.refreshProgress?.addToNumberOfTasksAndRemaining(webFeeds.count)
self.refresher.refreshFeeds(webFeeds) {
group.leave()
}
}
case .failure:
group.leave()
}
}
group.enter()
@ -104,9 +147,9 @@ private extension CloudKitArticlesZoneDelegate {
for receivedStarredArticle in receivedStarredArticles {
if let parsedItem = makeParsedItem(receivedStarredArticle) {
group.enter()
self.account?.update(parsedItem.feedURL, with: Set([parsedItem])) { databaseError in
self.account?.update(parsedItem.feedURL, with: Set([parsedItem])) { result in
group.leave()
if let databaseError = databaseError {
if case .failure(let databaseError) = result {
os_log(.error, log: self.log, "Error occurred while storing starred items: %@", databaseError.localizedDescription)
}
}
@ -119,7 +162,6 @@ private extension CloudKitArticlesZoneDelegate {
}
func makeParsedItem(_ articleRecord: CKRecord) -> ParsedItem? {
var parsedAuthors = Set<ParsedAuthor>()
@ -160,3 +202,17 @@ private extension CloudKitArticlesZoneDelegate {
}
}
extension CloudKitArticlesZoneDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, didProcess newAndUpdatedArticles: NewAndUpdatedArticles) {
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
refreshProgress?.completeTask()
}
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
}
}

View File

@ -14,7 +14,7 @@ import RSCore
class FeedFinder {
static func find(url: URL, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
downloadUsingCache(url) { (data, response, error) in
downloadAddingToCache(url) { (data, response, error) in
if response?.forcedStatusCode == 404 {
completion(.failure(AccountError.createErrorNotFound))
return

View File

@ -85,12 +85,16 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable {
public func hash(into hasher: inout Hasher) {
switch self {
case .smartFeed(let id):
hasher.combine("smartFeed")
hasher.combine(id)
case .script(let id):
hasher.combine("smartFeed")
hasher.combine(id)
case .webFeed(_, let webFeedID):
hasher.combine("webFeed")
hasher.combine(webFeedID)
case .folder(_, let folderName):
hasher.combine("folder")
hasher.combine(folderName)
}
}

View File

@ -125,15 +125,19 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
let results = StarredStatusResults()
group.enter()
account.markAsStarred(remoteStarredArticleIDs) { error in
results.markAsStarredError = error
account.markAsStarred(remoteStarredArticleIDs) { result in
if case .failure(let error) = result {
results.markAsStarredError = error
}
group.leave()
}
let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { error in
results.markAsUnstarredError = error
account.markAsUnstarred(deltaUnstarredArticleIDs) { result in
if case .failure(let error) = result {
results.markAsUnstarredError = error
}
group.leave()
}

View File

@ -125,15 +125,19 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
let results = ReadStatusResults()
group.enter()
account.markAsUnread(remoteUnreadArticleIDs) { error in
results.markAsUnreadError = error
account.markAsUnread(remoteUnreadArticleIDs) { result in
if case .failure(let error) = result {
results.markAsUnreadError = error
}
group.leave()
}
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
group.enter()
account.markAsRead(articleIDsToMarkRead) { error in
results.markAsReadError = error
account.markAsRead(articleIDsToMarkRead) { result in
if case .failure(let error) = result {
results.markAsReadError = error
}
group.leave()
}

View File

@ -10,6 +10,7 @@ import Foundation
import RSCore
import RSParser
import Articles
import ArticlesDatabase
import RSWeb
import Secrets
@ -19,7 +20,13 @@ public enum LocalAccountDelegateError: String, Error {
final class LocalAccountDelegate: AccountDelegate {
private let refresher = LocalAccountRefresher()
weak var account: Account?
private lazy var refresher: LocalAccountRefresher? = {
let refresher = LocalAccountRefresher()
refresher.delegate = self
return refresher
}()
let behaviors: AccountBehaviors = []
let isOPMLImportInProgress = false
@ -29,19 +36,23 @@ final class LocalAccountDelegate: AccountDelegate {
var accountMetadata: AccountMetadata?
let refreshProgress = DownloadProgress(numberOfTasks: 0)
var refreshAllCompletion: ((Result<Void, Error>) -> Void)? = nil
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
completion()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
guard refreshAllCompletion == nil else {
completion(.success(()))
return
}
refreshAllCompletion = completion
let webFeeds = account.flattenedWebFeeds()
refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count)
refresher.refreshFeeds(webFeeds, feedCompletionBlock: { _ in self.refreshProgress.completeTask() }) {
self.refreshProgress.clear()
account.metadata.lastArticleFetchEndTime = Date()
completion(.success(()))
}
refresher?.refreshFeeds(webFeeds)
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@ -197,6 +208,7 @@ final class LocalAccountDelegate: AccountDelegate {
}
func accountDidInitialize(_ account: Account) {
self.account = account
}
func accountWillBeDeleted(_ account: Account) {
@ -209,7 +221,7 @@ final class LocalAccountDelegate: AccountDelegate {
// MARK: Suspend and Resume (for iOS)
func suspendNetwork() {
refresher.suspend()
refresher?.suspend()
}
func suspendDatabase() {
@ -217,6 +229,24 @@ final class LocalAccountDelegate: AccountDelegate {
}
func resume() {
refresher.resume()
refresher?.resume()
}
}
extension LocalAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, didProcess newAndUpdatedArticles: NewAndUpdatedArticles) {
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
refreshProgress.completeTask()
}
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
self.refreshProgress.clear()
account?.metadata.lastArticleFetchEndTime = Date()
refreshAllCompletion?(.success(()))
refreshAllCompletion = nil
}
}

View File

@ -11,20 +11,28 @@ import RSCore
import RSParser
import RSWeb
import Articles
import ArticlesDatabase
protocol LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, didProcess: NewAndUpdatedArticles)
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed)
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher)
}
final class LocalAccountRefresher {
private var feedCompletionBlock: ((WebFeed) -> Void)?
private var completion: (() -> Void)?
private var completions = [() -> Void]()
private var isSuspended = false
var delegate: LocalAccountRefresherDelegate?
private lazy var downloadSession: DownloadSession = {
return DownloadSession(delegate: self)
}()
public func refreshFeeds(_ feeds: Set<WebFeed>, feedCompletionBlock: @escaping (WebFeed) -> Void, completion: @escaping () -> Void) {
self.feedCompletionBlock = feedCompletionBlock
self.completion = completion
public func refreshFeeds(_ feeds: Set<WebFeed>, completion: (() -> Void)? = nil) {
if let completion = completion {
completions.append(completion)
}
downloadSession.downloadObjects(feeds as NSSet)
}
@ -64,21 +72,21 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
guard !data.isEmpty, !isSuspended else {
completion()
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
if let error = error {
print("Error downloading \(feed.url) - \(error)")
completion()
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
let dataHash = data.md5String
if dataHash == feed.contentHash {
completion()
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
@ -87,20 +95,20 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
completion()
self.feedCompletionBlock?(feed)
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
account.update(feed, with: parsedFeed) { error in
if error == nil {
account.update(feed, with: parsedFeed) { result in
if case .success(let newAndUpdatedArticles) = result {
self.delegate?.localAccountRefresher(self, didProcess: newAndUpdatedArticles)
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
}
completion()
self.feedCompletionBlock?(feed)
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
}
@ -109,7 +117,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
let feed = representedObject as! WebFeed
guard !isSuspended else {
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return false
}
@ -118,7 +126,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
}
if data.isDefinitelyNotFeed() {
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return false
}
@ -127,7 +135,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
if FeedParser.mightBeAbleToParseBasedOnPartialData(parserData) {
return true
} else {
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return false
}
}
@ -137,17 +145,23 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
let feed = representedObject as! WebFeed
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
let feed = representedObject as! WebFeed
feedCompletionBlock?(feed)
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
let feed = representedObject as! WebFeed
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession) {
completion?()
completion = nil
completions.forEach({ $0() })
completions = [() -> Void]()
delegate?.localAccountRefresherDidFinish(self)
}
}

View File

@ -221,9 +221,9 @@ public final class WebFeed: Feed, Renamable, Hashable {
self.metadata = metadata
}
// MARK: - Debug
public func debugDropConditionalGetInfo() {
// MARK: - API
public func dropConditionalGetInfo() {
conditionalGetInfo = nil
contentHash = nil
}

View File

@ -27,6 +27,16 @@ public typealias SingleUnreadCountCompletionBlock = (SingleUnreadCountResult) ->
public struct NewAndUpdatedArticles {
public let newArticles: Set<Article>?
public let updatedArticles: Set<Article>?
public init() {
self.newArticles = Set<Article>()
self.updatedArticles = Set<Article>()
}
public init(newArticles: Set<Article>?, updatedArticles: Set<Article>?) {
self.newArticles = newArticles
self.updatedArticles = updatedArticles
}
}
public typealias UpdateArticlesResult = Result<NewAndUpdatedArticles, DatabaseError>
@ -222,8 +232,8 @@ public final class ArticlesDatabase {
return try articlesTable.mark(articles, statusKey, flag)
}
public func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping DatabaseCompletionBlock) {
articlesTable.mark(articleIDs, statusKey, flag, completion)
public func markAndFetchNew(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.markAndFetchNew(articleIDs, statusKey, flag, completion)
}
/// Create statuses for specified articleIDs. For existing statuses, dont do anything.

View File

@ -194,7 +194,7 @@ final class ArticlesTable: DatabaseTable {
func makeDatabaseCalls(_ database: FMDatabase) {
let articleIDs = parsedItems.articleIDs()
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
assert(statusesDictionary.count == articleIDs.count)
let allIncomingArticles = Article.articlesWithParsedItems(parsedItems, webFeedID, self.accountID, statusesDictionary) //2
@ -266,7 +266,7 @@ final class ArticlesTable: DatabaseTable {
articleIDs.formUnion(parsedItems.articleIDs())
}
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
let (statusesDictionary, _) = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
assert(statusesDictionary.count == articleIDs.count)
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedIDsAndItems, self.accountID, statusesDictionary) //2
@ -418,17 +418,17 @@ final class ArticlesTable: DatabaseTable {
return statuses
}
func mark(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ completion: @escaping DatabaseCompletionBlock) {
func markAndFetchNew(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ completion: @escaping ArticleIDsCompletionBlock) {
queue.runInTransaction { databaseResult in
switch databaseResult {
case .success(let database):
self.statusesTable.mark(articleIDs, statusKey, flag, database)
let newStatusIDs = self.statusesTable.markAndFetchNew(articleIDs, statusKey, flag, database)
DispatchQueue.main.async {
completion(nil)
completion(.success(newStatusIDs))
}
case .failure(let databaseError):
DispatchQueue.main.async {
completion(databaseError)
completion(.failure(databaseError))
}
}
}

View File

@ -27,11 +27,11 @@ final class StatusesTable: DatabaseTable {
// MARK: - Creating/Updating
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) -> [String: ArticleStatus] {
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) -> ([String: ArticleStatus], Set<String>) {
// Check cache.
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
if articleIDsMissingCachedStatus.isEmpty {
return statusesDictionary(articleIDs)
return (statusesDictionary(articleIDs), Set<String>())
}
// Check database.
@ -43,7 +43,7 @@ final class StatusesTable: DatabaseTable {
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, read, database)
}
return statusesDictionary(articleIDs)
return (statusesDictionary(articleIDs), articleIDsNeedingStatus)
}
func existingStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> [String: ArticleStatus] {
@ -85,10 +85,11 @@ final class StatusesTable: DatabaseTable {
return updatedStatuses
}
func mark(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
let statusesDictionary = ensureStatusesForArticleIDs(articleIDs, flag, database)
func markAndFetchNew(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set<String> {
let (statusesDictionary, newStatusIDs) = ensureStatusesForArticleIDs(articleIDs, flag, database)
let statuses = Set(statusesDictionary.values)
mark(statuses, statusKey, flag, database)
return newStatusIDs
}
// MARK: - Fetching

View File

@ -6,19 +6,19 @@
<body>
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
<outline text="Accidentally in Code" title="Accidentally in Code" type="rss" version="RSS" htmlUrl="https://cate.blog/" xmlUrl="https://cate.blog/feed/"/>
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="http://becky.coffee/" xmlUrl="https://beckyhansmeyer.com/feed/"/>
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
<outline text="The Shape of Everything" title="The Shape of Everything" type="rss" version="RSS" htmlUrl="https://shapeof.com/" xmlUrl="https://shapeof.com/feed.json"/>
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
<outline text="Manton Reece" title="Manton Reece" type="rss" version="RSS" htmlUrl="https://manton.org/" xmlUrl="https://www.manton.org/feed/json"/>
<outline text="Julia Evans" title="Julia Evans" type="rss" version="RSS" htmlUrl="https://jvns.ca/" xmlUrl="https://jvns.ca/atom.xml"/>
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://sixcolors.com/feed.json"/>
<outline text="Loop Insight" title="Loop Insight" type="rss" version="RSS" htmlUrl="http://www.loopinsight.com/" xmlUrl="http://www.loopinsight.com/feed/"/>
<outline text="Loop Insight" title="Loop Insight" type="rss" version="RSS" htmlUrl="https://www.loopinsight.com/" xmlUrl="https://www.loopinsight.com/feed/"/>
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://nnw.ranchero.com/" xmlUrl="https://nnw.ranchero.com/feed.json"/>
<outline text="Erica Sadun" title="Erica Sadun" type="rss" version="RSS" htmlUrl="https://ericasadun.com/" xmlUrl="https://ericasadun.com/feed/"/>
<outline text="One Foot Tsunami" title="One Foot Tsunami" type="rss" version="RSS" htmlUrl="https://onefoottsunami.com/" xmlUrl="https://onefoottsunami.com/feed/json/"/>
<outline text="The Omni Blog" title="The Omni Blog" type="rss" version="RSS" htmlUrl="https://www.omnigroup.com/blog/" xmlUrl="https://www.omnigroup.com/blog/rss/"/>
<outline text="Rose Orchard" title="Rose Orchard" type="rss" version="RSS" htmlUrl="https://rosemaryorchard.com/" xmlUrl="https://rosemaryorchard.com/category/blog/feed"/>
<outline text="Craig Hockenberry" title="Craig Hockenberry" type="rss" version="RSS" htmlUrl="https://furbo.org/" xmlUrl="https://furbo.org/feed/json"/>
<outline text="Rose Orchard" title="Rose Orchard" type="rss" version="RSS" htmlUrl="https://rosemaryorchard.com/" xmlUrl="https://rosemaryorchard.com/blog/feed/"/>
<outline text="Michael Tsai" title="Michael Tsai" type="rss" version="RSS" htmlUrl="https://mjtsai.com/blog/" xmlUrl="https://mjtsai.com/blog/feed/"/>
</body>
</opml>

View File

@ -191,6 +191,7 @@ open class ImageScrollView: UIScrollView {
zoomView!.addGestureRecognizer(upSwipeGesture)
configureImageForSize(image.size)
adjustFrameToCenter()
}
private func configureImageForSize(_ size: CGSize) {

View File

@ -36,7 +36,7 @@ class ImageViewer {
var canvas = document.createElement("canvas");
canvas.width = this.img.naturalWidth;
canvas.height = this.img.naturalHeight;
canvas.getContext("2d").drawImage(this.img, 0, 0);
canvas.getContext("2d").drawImage(this.img, 0, 0, canvas.width, canvas.height);
const rect = this.img.getBoundingClientRect();
const message = {

@ -1 +1 @@
Subproject commit 88d634f5fd42aab203b6e53c7b551a92b03ffc97
Subproject commit c524ce9145dfe093500325b1c758ea83f82cc090