Convert NewsBlur to async await.
This commit is contained in:
parent
be4564716f
commit
d58821a7ad
|
@ -19,29 +19,22 @@ import CommonErrors
|
|||
|
||||
extension NewsBlurAccountDelegate {
|
||||
|
||||
func refreshFeeds(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
os_log(.debug, log: log, "Refreshing feeds...")
|
||||
func refreshFeeds(for account: Account) async throws {
|
||||
|
||||
caller.retrieveFeeds { result in
|
||||
os_log(.debug, log: log, "Refreshing feeds…")
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
switch result {
|
||||
case .success((let feeds, let folders)):
|
||||
BatchUpdate.shared.perform {
|
||||
self.syncFolders(account, folders)
|
||||
self.syncFeeds(account, feeds)
|
||||
self.syncFeedFolderRelationship(account, folders)
|
||||
}
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
let (feeds, folders) = try await caller.retrieveFeeds()
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
self.syncFolders(account, folders)
|
||||
self.syncFeeds(account, feeds)
|
||||
self.syncFeedFolderRelationship(account, folders)
|
||||
}
|
||||
}
|
||||
|
||||
func syncFolders(_ account: Account, _ folders: [NewsBlurFolder]?) {
|
||||
guard let folders = folders else { return }
|
||||
|
||||
guard let folders else { return }
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
os_log(.debug, log: log, "Syncing folders with %ld folders.", folders.count)
|
||||
|
@ -79,7 +72,7 @@ extension NewsBlurAccountDelegate {
|
|||
}
|
||||
|
||||
func syncFeeds(_ account: Account, _ feeds: [NewsBlurFeed]?) {
|
||||
guard let feeds = feeds else { return }
|
||||
guard let feeds else { return }
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
os_log(.debug, log: log, "Syncing feeds with %ld feeds.", feeds.count)
|
||||
|
@ -130,7 +123,8 @@ extension NewsBlurAccountDelegate {
|
|||
}
|
||||
|
||||
func syncFeedFolderRelationship(_ account: Account, _ folders: [NewsBlurFolder]?) {
|
||||
guard let folders = folders else { return }
|
||||
|
||||
guard let folders else { return }
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
os_log(.debug, log: log, "Syncing folders with %ld folders.", folders.count)
|
||||
|
@ -231,44 +225,23 @@ extension NewsBlurAccountDelegate {
|
|||
return d
|
||||
}
|
||||
|
||||
func refreshUnreadStories(for account: Account, hashes: [NewsBlurStoryHash]?, updateFetchDate: Date?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let hashes = hashes, !hashes.isEmpty else {
|
||||
func refreshUnreadStories(for account: Account, hashes: [NewsBlurStoryHash]?, updateFetchDate: Date?) async throws {
|
||||
|
||||
guard let hashes, !hashes.isEmpty else {
|
||||
if let lastArticleFetch = updateFetchDate {
|
||||
self.accountMetadata?.lastArticleFetchStartTime = lastArticleFetch
|
||||
self.accountMetadata?.lastArticleFetchEndTime = Date()
|
||||
}
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let numberOfStories = min(hashes.count, 100) // api limit
|
||||
let hashesToFetch = Array(hashes[..<numberOfStories])
|
||||
|
||||
caller.retrieveStories(hashes: hashesToFetch) { result in
|
||||
switch result {
|
||||
case .success((let stories, let date)):
|
||||
self.processStories(account: account, stories: stories) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if case .failure(let error) = result {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshUnreadStories(for: account, hashes: Array(hashes[numberOfStories...]), updateFetchDate: date) { result in
|
||||
os_log(.debug, log: self.log, "Done refreshing stories.")
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
let (stories, date) = try await caller.retrieveStories(hashes: hashesToFetch)
|
||||
try await processStories(account: account, stories: stories)
|
||||
try await refreshUnreadStories(for: account, hashes: Array(hashes[numberOfStories...]), updateFetchDate: date)
|
||||
os_log(.debug, log: self.log, "Done refreshing stories.")
|
||||
}
|
||||
|
||||
func mapStoriesToParsedItems(stories: [NewsBlurStory]?) -> Set<ParsedItem> {
|
||||
|
@ -282,251 +255,170 @@ extension NewsBlurAccountDelegate {
|
|||
return Set(parsedItems)
|
||||
}
|
||||
|
||||
func sendStoryStatuses(_ statuses: [SyncStatus],
|
||||
throttle: Bool,
|
||||
apiCall: ([String], @escaping (Result<Void, Error>) -> Void) -> Void,
|
||||
completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func sendStoryStatuses(_ statuses: Set<SyncStatus>, throttle: Bool, apiCall: (Set<String>) async throws -> Void) async throws {
|
||||
|
||||
guard !statuses.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let storyHashes = statuses.compactMap { $0.articleID }
|
||||
let storyHashGroups = storyHashes.chunked(into: throttle ? 1 : 5) // api limit
|
||||
for storyHashGroup in storyHashGroups {
|
||||
group.enter()
|
||||
apiCall(storyHashGroup) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
Task {
|
||||
try? await self.database.deleteSelectedForProcessing(storyHashGroup.map { String($0) } )
|
||||
group.leave()
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Story status sync call failed: %@.", error.localizedDescription)
|
||||
Task {
|
||||
try? await self.database.resetSelectedForProcessing(storyHashGroup.map { String($0) } )
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try await apiCall(Set(storyHashGroup))
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Story status sync call failed: %@.", error.localizedDescription)
|
||||
try? await syncDatabase.resetSelectedForProcessing(storyHashGroup.map { String($0) } )
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
if errorOccurred {
|
||||
throw NewsBlurError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
func syncStoryReadState(account: Account, hashes: [NewsBlurStoryHash]?, completion: @escaping (() -> Void)) {
|
||||
func syncStoryReadState(account: Account, hashes: Set<NewsBlurStoryHash>?) async {
|
||||
|
||||
guard let hashes else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let pendingArticleIDs = (try await self.database.selectPendingReadStatusArticleIDs()) ?? Set<String>()
|
||||
do {
|
||||
let pendingArticleIDs = (try await syncDatabase.selectPendingReadStatusArticleIDs()) ?? Set<String>()
|
||||
|
||||
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingArticleIDs)
|
||||
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingArticleIDs)
|
||||
|
||||
guard let currentUnreadArticleIDs = try await account.fetchUnreadArticleIDs() else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
|
||||
try? await account.markAsUnread(deltaUnreadArticleIDs)
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
try? await account.markAsRead(deltaReadArticleIDs)
|
||||
|
||||
completion()
|
||||
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
||||
guard let currentUnreadArticleIDs = try await account.fetchUnreadArticleIDs() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
|
||||
try? await account.markAsUnread(deltaUnreadArticleIDs)
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
try? await account.markAsRead(deltaReadArticleIDs)
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func syncStoryStarredState(account: Account, hashes: [NewsBlurStoryHash]?, completion: @escaping (() -> Void)) {
|
||||
guard let hashes = hashes else {
|
||||
completion()
|
||||
func syncStoryStarredState(account: Account, hashes: Set<NewsBlurStoryHash>?) async {
|
||||
|
||||
guard let hashes else {
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let pendingArticleIDs = (try await syncDatabase.selectPendingStarredStatusArticleIDs()) ?? Set<String>()
|
||||
|
||||
do {
|
||||
let pendingArticleIDs = (try await self.database.selectPendingStarredStatusArticleIDs()) ?? Set<String>()
|
||||
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingArticleIDs)
|
||||
|
||||
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingArticleIDs)
|
||||
|
||||
guard let currentStarredArticleIDs = try await account.fetchStarredArticleIDs() else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
||||
try? await account.markAsStarred(deltaStarredArticleIDs)
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
try? await account.markAsUnstarred(deltaUnstarredArticleIDs)
|
||||
|
||||
completion()
|
||||
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
||||
guard let currentStarredArticleIDs = try await account.fetchStarredArticleIDs() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
||||
try? await account.markAsStarred(deltaStarredArticleIDs)
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
try? await account.markAsUnstarred(deltaUnstarredArticleIDs)
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
guard let feed = feed else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
func createFeed(account: Account, newsBlurFeed: NewsBlurFeed, name: String?, container: Container) async throws -> Feed {
|
||||
|
||||
let feed = account.createFeed(with: newsBlurFeed.name, url: newsBlurFeed.feedURL, feedID: String(newsBlurFeed.feedID), homePageURL: newsBlurFeed.homePageURL)
|
||||
feed.externalID = String(newsBlurFeed.feedID)
|
||||
feed.faviconURL = newsBlurFeed.faviconURL
|
||||
|
||||
try await account.addFeed(feed, to: container)
|
||||
if let name {
|
||||
try await renameFeed(for: account, with: feed, to: name)
|
||||
}
|
||||
try await initialFeedDownload(account: account, feed: feed)
|
||||
return feed
|
||||
}
|
||||
|
||||
func downloadFeed(account: Account, feed: Feed, page: Int) async throws {
|
||||
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
let (stories, _) = try await caller.retrieveStories(feedID: feed.feedID, page: page)
|
||||
refreshProgress.completeTask()
|
||||
|
||||
guard let stories, stories.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
let feed = account.createFeed(with: feed.name, url: feed.feedURL, feedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
feed.externalID = String(feed.feedID)
|
||||
feed.faviconURL = feed.faviconURL
|
||||
let since: Date? = Calendar.current.date(byAdding: .month, value: -3, to: Date())
|
||||
|
||||
do {
|
||||
try await account.addFeed(feed, to: container)
|
||||
if let name {
|
||||
try await self.renameFeed(for: account, with: feed, to: name)
|
||||
}
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
let hasStories = try await processStories(account: account, stories: stories, since: since)
|
||||
if hasStories {
|
||||
try await downloadFeed(account: account, feed: feed, page: page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFeed(account: Account, feed: Feed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
func initialFeedDownload(account: Account, feed: Feed) async throws {
|
||||
|
||||
caller.retrieveStories(feedID: feed.feedID, page: page) { result in
|
||||
switch result {
|
||||
case .success((let stories, _)):
|
||||
// No more stories
|
||||
guard let stories = stories, stories.count > 0 else {
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let since: Date? = Calendar.current.date(byAdding: .month, value: -3, to: Date())
|
||||
|
||||
self.processStories(account: account, stories: stories, since: since) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if case .failure(let error) = result {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// No more recent stories
|
||||
if case .success(let hasStories) = result, !hasStories {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
self.downloadFeed(account: account, feed: feed, page: page + 1, completion: completion)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
}
|
||||
|
||||
func initialFeedDownload(account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
// Download the initial articles
|
||||
downloadFeed(account: account, feed: feed, page: 1) { result in
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.refreshArticleStatus(for: account)
|
||||
self.refreshMissingStories(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
try await downloadFeed(account: account, feed: feed, page: 1)
|
||||
try await refreshArticleStatus(for: account)
|
||||
try await refreshMissingStories(for: account)
|
||||
}
|
||||
|
||||
func deleteFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func deleteFeed(for account: Account, with feed: Feed, from container: Container?) async throws {
|
||||
|
||||
// This error should never happen
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
let folderName = (container as? Folder)?.name
|
||||
caller.deleteFeed(feedID: feedID, folder: folderName) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
let feedID = feed.feedID
|
||||
do {
|
||||
try await caller.deleteFeed(feedID: feedID, folder: folderName)
|
||||
|
||||
if folderName == nil {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
if folderName == nil {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
|
||||
if let folders = account.folders {
|
||||
for folder in folders where folderName != nil && folder.name == folderName {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
if account.existingFeed(withFeedID: feedID) != nil {
|
||||
account.clearFeedMetadata(feed)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
if let folders = account.folders {
|
||||
for folder in folders where folderName != nil && folder.name == folderName {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
if account.existingFeed(withFeedID: feed.feedID) != nil {
|
||||
account.clearFeedMetadata(feed)
|
||||
}
|
||||
|
||||
} catch {
|
||||
throw AccountError.wrappedError(error: error, account: account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
|
||||
let caller: NewsBlurAPICaller
|
||||
let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "NewsBlur")
|
||||
let database: SyncDatabase
|
||||
let syncDatabase: SyncDatabase
|
||||
|
||||
init(dataFolder: String, transport: Transport?) {
|
||||
if let transport = transport {
|
||||
|
@ -53,320 +53,164 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
caller = NewsBlurAPICaller(transport: session)
|
||||
}
|
||||
|
||||
database = SyncDatabase(databasePath: dataFolder.appending("/DB.sqlite3"))
|
||||
syncDatabase = SyncDatabase(databasePath: dataFolder.appending("/DB.sqlite3"))
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any]) async {
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account) async throws {
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(4)
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
try await refreshFeeds(for: account)
|
||||
refreshProgress.completeTask()
|
||||
|
||||
self.refreshAll(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try await sendArticleStatus(for: account)
|
||||
refreshProgress.completeTask()
|
||||
|
||||
private func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(4)
|
||||
try await refreshArticleStatus(for: account)
|
||||
refreshProgress.completeTask()
|
||||
|
||||
refreshFeeds(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshMissingStories(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
try await refreshMissingStories(for: account)
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
try await sendArticleStatus(for: account)
|
||||
try await refreshArticleStatus(for: account)
|
||||
}
|
||||
|
||||
|
||||
public func sendArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
os_log(.debug, log: log, "Sending story statuses…")
|
||||
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
let syncStatuses = (try await self.syncDatabase.selectForProcessing()) ?? Set<SyncStatus>()
|
||||
|
||||
let createUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.read && $0.flag == false
|
||||
}
|
||||
let deleteUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.read && $0.flag == true
|
||||
}
|
||||
let createStarredStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.starred && $0.flag == true
|
||||
}
|
||||
let deleteStarredStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.starred && $0.flag == false
|
||||
}
|
||||
}
|
||||
|
||||
private func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
os_log(.debug, log: log, "Sending story statuses...")
|
||||
|
||||
Task { @MainActor in
|
||||
var errorOccurred = false
|
||||
|
||||
do {
|
||||
let syncStatuses = (try await self.database.selectForProcessing()) ?? Set<SyncStatus>()
|
||||
do {
|
||||
try await sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: caller.markAsUnread)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
let createUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.read && $0.flag == false
|
||||
}
|
||||
let deleteUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.read && $0.flag == true
|
||||
}
|
||||
let createStarredStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.starred && $0.flag == true
|
||||
}
|
||||
let deleteStarredStatuses = syncStatuses.filter {
|
||||
$0.key == SyncStatus.Key.starred && $0.flag == false
|
||||
}
|
||||
do {
|
||||
try await sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: caller.markAsRead)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
do {
|
||||
try await sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: caller.star)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
do {
|
||||
try await sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: caller.unstar)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processStatuses(Array(syncStatuses))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
throw NewsBlurError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
os_log(.debug, log: log, "Refreshing story statuses…")
|
||||
|
||||
private func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
os_log(.debug, log: log, "Refreshing story statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
caller.retrieveUnreadStoryHashes { result in
|
||||
switch result {
|
||||
case .success(let storyHashes):
|
||||
self.syncStoryReadState(account: account, hashes: storyHashes) {
|
||||
group.leave()
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving unread stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
do {
|
||||
let storyHashes = try await caller.retrieveUnreadStoryHashes()
|
||||
await syncStoryReadState(account: account, hashes: storyHashes)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving unread stories failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
group.enter()
|
||||
caller.retrieveStarredStoryHashes { result in
|
||||
switch result {
|
||||
case .success(let storyHashes):
|
||||
self.syncStoryStarredState(account: account, hashes: storyHashes) {
|
||||
group.leave()
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving starred stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
do {
|
||||
let storyHashes = try await caller.retrieveStarredStoryHashes()
|
||||
await syncStoryStarredState(account: account, hashes: storyHashes)
|
||||
} catch {
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving starred stories failed: %@.", error.localizedDescription)
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||
if errorOccurred {
|
||||
throw NewsBlurError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
func refreshStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
os_log(.debug, log: log, "Refreshing stories...")
|
||||
os_log(.debug, log: log, "Refreshing unread stories...")
|
||||
func refreshStories(for account: Account) async throws {
|
||||
|
||||
caller.retrieveUnreadStoryHashes { result in
|
||||
switch result {
|
||||
case .success(let storyHashes):
|
||||
os_log(.debug, log: log, "Refreshing stories…")
|
||||
os_log(.debug, log: log, "Refreshing unread stories…")
|
||||
|
||||
if let count = storyHashes?.count, count > 0 {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining((count - 1) / 100 + 1)
|
||||
}
|
||||
|
||||
self.refreshUnreadStories(for: account, hashes: storyHashes, updateFetchDate: nil, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
let storyHashes = try await caller.retrieveUnreadStoryHashes()
|
||||
if let count = storyHashes?.count, count > 0 {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining((count - 1) / 100 + 1)
|
||||
}
|
||||
|
||||
let storyHashesArray: [NewsBlurStoryHash] = {
|
||||
if let storyHashes {
|
||||
return Array(storyHashes)
|
||||
}
|
||||
return [NewsBlurStoryHash]()
|
||||
}()
|
||||
try await refreshUnreadStories(for: account, hashes: storyHashesArray, updateFetchDate: nil)
|
||||
}
|
||||
|
||||
func refreshMissingStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
os_log(.debug, log: log, "Refreshing missing stories...")
|
||||
func refreshMissingStories(for account: Account) async throws {
|
||||
|
||||
Task { @MainActor in
|
||||
os_log(.debug, log: log, "Refreshing missing stories…")
|
||||
|
||||
let fetchedArticleIDs = try await account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() ?? Set<String>()
|
||||
|
||||
var errorOccurred = false
|
||||
|
||||
let storyHashes = Array(fetchedArticleIDs).map {
|
||||
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||
}
|
||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedStoryHashes {
|
||||
do {
|
||||
let fetchedArticleIDs = try await account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() ?? Set<String>()
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let storyHashes = Array(fetchedArticleIDs).map {
|
||||
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||
}
|
||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedStoryHashes {
|
||||
group.enter()
|
||||
self.caller.retrieveStories(hashes: chunk) { result in
|
||||
|
||||
switch result {
|
||||
case .success((let stories, _)):
|
||||
self.processStories(account: account, stories: stories) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing stories.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
let (stories, _) = try await caller.retrieveStories(hashes: chunk)
|
||||
try await processStories(account: account, stories: stories)
|
||||
} catch {
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.failure(error))
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
os_log(.debug, log: self.log, "Done refreshing missing stories.")
|
||||
if errorOccurred {
|
||||
throw NewsBlurError.unknown
|
||||
}
|
||||
}
|
||||
|
||||
func processStories(account: Account, stories: [NewsBlurStory]?, since: Date? = nil, completion: @escaping (Result<Bool, DatabaseError>) -> Void) {
|
||||
@discardableResult
|
||||
func processStories(account: Account, stories: [NewsBlurStory]?, since: Date? = nil) async throws -> Bool {
|
||||
|
||||
let parsedItems = mapStoriesToParsedItems(stories: stories).filter {
|
||||
guard let datePublished = $0.datePublished, let since = since else {
|
||||
|
@ -379,14 +223,8 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
Set($0)
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true)
|
||||
completion(.success(!feedIDsAndItems.isEmpty))
|
||||
} catch {
|
||||
completion(.failure(.suspended))
|
||||
}
|
||||
}
|
||||
try await account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true)
|
||||
return !feedIDsAndItems.isEmpty
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL) async throws {
|
||||
|
@ -394,97 +232,44 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
|
||||
func createFolder(for account: Account, name: String) async throws -> Folder {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
refreshProgress.addTask()
|
||||
|
||||
self.createFolder(for: account, name: name) { result in
|
||||
switch result {
|
||||
case .success(let folder):
|
||||
continuation.resume(returning: folder)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try await caller.addFolder(named: name)
|
||||
refreshProgress.completeTask()
|
||||
|
||||
private func createFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> ()) {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.addFolder(named: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success():
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
return folder
|
||||
} else {
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.renameFolder(for: account, with: folder, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folderToRename = folder.name else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
let nameBefore = folder.name
|
||||
|
||||
caller.renameFolder(with: folderToRename, to: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
folder.name = nameBefore
|
||||
completion(.failure(error))
|
||||
}
|
||||
do {
|
||||
try await caller.renameFolder(with: folderToRename, to: name)
|
||||
folder.name = name
|
||||
} catch {
|
||||
folder.name = nameBefore
|
||||
throw error
|
||||
}
|
||||
|
||||
folder.name = name
|
||||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.removeFolder(for: account, with: folder) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folderToRemove = folder.name else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
|
||||
var feedIDs: [String] = []
|
||||
|
@ -496,246 +281,105 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.removeFolder(named: folderToRemove, feedIDs: feedIDs) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
account.removeFolder(folder: folder)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
try await caller.removeFolder(named: folderToRemove, feedIDs: feedIDs)
|
||||
account.removeFolder(folder: folder)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool) async throws -> Feed {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.createFeed(for: account, url: url, name: name, container: container, validateFeed: validateFeed) { result in
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
continuation.resume(returning: feed)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
}
|
||||
|
||||
private func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> ()) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
let folderName = (container as? Folder)?.name
|
||||
caller.addURL(url, folder: folderName) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
self.createFeed(account: account, feed: feed, name: name, container: container, completion: completion)
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
do {
|
||||
guard let newsBlurFeed = try await caller.addURL(url, folder: folderName) else {
|
||||
throw NewsBlurError.unknown
|
||||
}
|
||||
let feed = try await createFeed(account: account, newsBlurFeed: newsBlurFeed, name: name, container: container)
|
||||
return feed
|
||||
} catch {
|
||||
throw AccountError.wrappedError(error: error, account: account)
|
||||
}
|
||||
}
|
||||
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.renameFeed(for: account, with: feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
caller.renameFeed(feedID: feedID, newName: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
feed.editedName = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
do {
|
||||
try await caller.renameFeed(feedID: feedID, newName: name)
|
||||
feed.editedName = name
|
||||
} catch {
|
||||
throw AccountError.wrappedError(error: error, account: account)
|
||||
}
|
||||
}
|
||||
|
||||
func addFeed(for account: Account, with feed: Feed, to container: any Container) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.addFeed(for: account, with: feed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folder = container as? Folder else {
|
||||
DispatchQueue.main.async {
|
||||
if let account = container as? Account {
|
||||
account.addFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
if let account = container as? Account {
|
||||
account.addFeed(feed)
|
||||
return
|
||||
}
|
||||
|
||||
guard let folder = container as? Folder else {
|
||||
return
|
||||
}
|
||||
let folderName = folder.name ?? ""
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: folderName)
|
||||
folder.addFeed(feed)
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: any Container) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.removeFeed(for: account, with: feed, from: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
deleteFeed(for: account, with: feed, from: container, completion: completion)
|
||||
try await deleteFeed(for: account, with: feed, from: container)
|
||||
}
|
||||
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.moveFeed(for: account, with: feed, from: from, to: to) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.moveFeed(
|
||||
feedID: feedID,
|
||||
from: (from as? Folder)?.name,
|
||||
to: (to as? Folder)?.name
|
||||
) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
refreshProgress.addTask()
|
||||
defer {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
|
||||
try await caller.moveFeed( feedID: feedID, from: (from as? Folder)?.name, to: (to as? Folder)?.name)
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, container: any Container) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.restoreFeed(for: account, feed: feed, container: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
Task { @MainActor in
|
||||
|
||||
do {
|
||||
try await account.addFeed(existingFeed, to: container)
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
return try await account.addFeed(existingFeed, to: container)
|
||||
} else {
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
try await createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true)
|
||||
}
|
||||
}
|
||||
|
||||
func restoreFolder(for account: Account, folder: Folder) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.restoreFolder(for: account, folder: folder) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folderName = folder.name else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
throw NewsBlurError.invalidParameter
|
||||
}
|
||||
|
||||
var feedsToRestore: [Feed] = []
|
||||
|
@ -744,71 +388,33 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
folder.topLevelFeeds.remove(feed)
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
createFolder(for: account, name: folderName) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success(let folder):
|
||||
for feed in feedsToRestore {
|
||||
group.enter()
|
||||
self.restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
do {
|
||||
let folder = try await createFolder(for: account, name: folderName)
|
||||
for feed in feedsToRestore {
|
||||
do {
|
||||
try await restoreFeed(for: account, feed: feed, container: folder)
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
os_log(.error, log: self.log, "Restore folder error: %@.", error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) async throws {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.markArticles(for: account, articles: articles, statusKey: statusKey, flag: flag) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
let articles = try await account.update(articles: articles, statusKey: statusKey, flag: flag)
|
||||
|
||||
let syncStatuses = articles.map { article in
|
||||
return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
|
||||
}
|
||||
}
|
||||
try? await syncDatabase.insertStatuses(syncStatuses)
|
||||
|
||||
private func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
Task { @MainActor in
|
||||
|
||||
do {
|
||||
|
||||
let articles = try await account.update(articles: articles, statusKey: statusKey, flag: flag)
|
||||
|
||||
let syncStatuses = articles.map { article in
|
||||
return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
|
||||
}
|
||||
|
||||
try? await self.database.insertStatuses(syncStatuses)
|
||||
|
||||
if let count = try? await self.database.selectPendingCount(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
completion(.success(()))
|
||||
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
if let count = try? await syncDatabase.selectPendingCount(), count > 100 {
|
||||
try await sendArticleStatus(for: account)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -817,32 +423,16 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
caller.logout() { _ in }
|
||||
Task { @MainActor in
|
||||
try await caller.logout()
|
||||
}
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider) async throws -> Credentials? {
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
||||
self.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, secretsProvider: secretsProvider) { result in
|
||||
switch result {
|
||||
case .success(let credentials):
|
||||
continuation.resume(returning: credentials)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> ()) {
|
||||
let caller = NewsBlurAPICaller(transport: transport)
|
||||
caller.credentials = credentials
|
||||
caller.validateCredentials() { result in
|
||||
DispatchQueue.main.async {
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
return try await caller.validateCredentials()
|
||||
}
|
||||
|
||||
// MARK: Suspend and Resume (for iOS)
|
||||
|
@ -856,7 +446,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
func suspendDatabase() {
|
||||
|
||||
Task {
|
||||
await database.suspend()
|
||||
await syncDatabase.suspend()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -865,7 +455,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||
|
||||
Task {
|
||||
caller.resume()
|
||||
await database.resume()
|
||||
await syncDatabase.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
|
||||
public struct NewsBlurStoryStatusChange: Sendable {
|
||||
|
||||
public let hashes: [String]
|
||||
public let hashes: Set<String>
|
||||
}
|
||||
|
||||
extension NewsBlurStoryStatusChange: NewsBlurDataConvertible {
|
||||
|
|
|
@ -78,7 +78,7 @@ import Secrets
|
|||
return (payload?.feeds, payload?.folders)
|
||||
}
|
||||
|
||||
func retrieveStoryHashes(endpoint: String) async throws -> [NewsBlurStoryHash]? {
|
||||
func retrieveStoryHashes(endpoint: String) async throws -> Set<NewsBlurStoryHash>? {
|
||||
|
||||
let url: URL! = baseURL
|
||||
.appendingPathComponent(endpoint)
|
||||
|
@ -88,16 +88,19 @@ import Secrets
|
|||
|
||||
let (_, payload) = try await requestData(callURL: url, resultType: NewsBlurStoryHashesResponse.self, dateDecoding: .secondsSince1970)
|
||||
|
||||
let hashes = payload?.unread ?? payload?.starred
|
||||
return hashes
|
||||
if let hashes = payload?.unread ?? payload?.starred {
|
||||
return Set(hashes)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func retrieveUnreadStoryHashes() async throws -> [NewsBlurStoryHash]? {
|
||||
public func retrieveUnreadStoryHashes() async throws -> Set<NewsBlurStoryHash>? {
|
||||
|
||||
return try await retrieveStoryHashes(endpoint: "reader/unread_story_hashes")
|
||||
}
|
||||
|
||||
public func retrieveStarredStoryHashes() async throws -> [NewsBlurStoryHash]? {
|
||||
public func retrieveStarredStoryHashes() async throws -> Set<NewsBlurStoryHash>? {
|
||||
|
||||
return try await retrieveStoryHashes(endpoint: "reader/starred_story_hashes")
|
||||
}
|
||||
|
@ -131,22 +134,22 @@ import Secrets
|
|||
return (payload?.stories, HTTPDateInfo(urlResponse: response)?.date)
|
||||
}
|
||||
|
||||
public func markAsUnread(hashes: [String]) async throws {
|
||||
public func markAsUnread(hashes: Set<String>) async throws {
|
||||
|
||||
try await sendUpdates(endpoint: "reader/mark_story_hash_as_unread", payload: NewsBlurStoryStatusChange(hashes: hashes))
|
||||
}
|
||||
|
||||
public func markAsRead(hashes: [String]) async throws {
|
||||
public func markAsRead(hashes: Set<String>) async throws {
|
||||
|
||||
try await sendUpdates(endpoint: "reader/mark_story_hashes_as_read", payload: NewsBlurStoryStatusChange(hashes: hashes))
|
||||
}
|
||||
|
||||
public func star(hashes: [String]) async throws {
|
||||
public func star(hashes: Set<String>) async throws {
|
||||
|
||||
try await sendUpdates(endpoint: "reader/mark_story_hash_as_starred", payload: NewsBlurStoryStatusChange(hashes: hashes))
|
||||
}
|
||||
|
||||
public func unstar(hashes: [String]) async throws {
|
||||
public func unstar(hashes: Set<String>) async throws {
|
||||
|
||||
try await sendUpdates(endpoint: "reader/mark_story_hash_as_unstarred", payload: NewsBlurStoryStatusChange(hashes: hashes))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue