diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index b911d2ee8..d555a5152 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -303,6 +303,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, switch result { case .success: guard let self = self else { return } + // Reset the last fetch date to get the article history for the added feeds. + self.metadata.lastArticleFetch = nil self.delegate.refreshAll(for: self) { completion(.success(())) } diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index bb4424072..a86e081b8 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133230F22810E5700C30F19 /* FeedbinIcon.swift */; }; 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; }; 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; }; + 5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */; }; 5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */; }; 5165D71622821C2400D9D53D /* taggings_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71322821C2400D9D53D /* taggings_delete.json */; }; 5165D71722821C2400D9D53D /* taggings_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71422821C2400D9D53D /* taggings_add.json */; }; @@ -116,6 +117,7 @@ 5133230F22810E5700C30F19 /* FeedbinIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinIcon.swift; sourceTree = ""; }; 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = ""; }; 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = ""; }; + 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = ""; }; 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderContentsSyncTest.swift; sourceTree = ""; }; 5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = ""; }; 5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = ""; }; @@ -255,6 +257,7 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */, 84CAD7151FDF2E22000F0755 /* FeedbinEntry.swift */, 5133230F22810E5700C30F19 /* FeedbinIcon.swift */, + 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */, 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */, 84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */, 51D58754227F53BE00900287 /* FeedbinTag.swift */, @@ -521,6 +524,7 @@ 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, 5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */, 844B297D2106C7EC004020B3 /* Feed.swift in Sources */, + 5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */, 84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */, 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index 17a043433..695269669 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -67,6 +67,54 @@ final class FeedbinAPICaller: NSObject { } + func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("imports.json") + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in + + switch result { + case .success(let (_, data)): + + guard let resultData = data else { + completion(.failure(TransportError.noData)) + break + } + + do { + let result = try JSONDecoder().decode(FeedbinImportResult.self, from: resultData) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("imports/\(importID).json") + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request, resultType: FeedbinImportResult.self) { result in + + switch result { + case .success(let (_, importResult)): + completion(.success(importResult)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("tags.json") diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index ff1851554..7ab706a6d 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -185,7 +185,7 @@ final class FeedbinAccountDelegate: AccountDelegate { } func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { - + var fileData: Data? do { @@ -200,27 +200,66 @@ final class FeedbinAccountDelegate: AccountDelegate { return } - let parserData = ParserData(url: opmlFile.absoluteString, data: opmlData) - var opmlDocument: RSOPMLDocument? - - do { - opmlDocument = try RSOPMLParser.parseOPML(with: parserData) - } catch { - completion(.failure(error)) - return - } - - guard let loadDocument = opmlDocument, let children = loadDocument.children else { - completion(.success(())) - return - } - - importOPMLItems(account, items: children, parentFolder: nil) + os_log(.debug, log: log, "Begin importing OPML...") - completion(.success(())) + caller.importOPML(opmlData: opmlData) { [weak self] result in + switch result { + case .success(let importResult): + if importResult.complete { + guard let self = self else { return } + os_log(.debug, log: self.log, "Import OPML done.") + DispatchQueue.main.async { + completion(.success(())) + } + } else { + self?.checkImportResult(opmlImportResultID: importResult.importResultID, completion: completion) + } + case .failure(let error): + guard let self = self else { return } + os_log(.debug, log: self.log, "Import OPML failed.") + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } } + private func checkImportResult(opmlImportResultID: Int, completion: @escaping (Result) -> Void) { + + DispatchQueue.main.async { + + Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] timer in + + guard let self = self else { return } + + os_log(.debug, log: self.log, "Checking status of OPML import...") + + self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) { result in + switch result { + case .success(let importResult): + if let result = importResult, result.complete { + os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.") + timer.invalidate() + DispatchQueue.main.async { + completion(.success(())) + } + } + case .failure(let error): + os_log(.debug, log: self.log, "Import OPML check failed.") + timer.invalidate() + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + + } + + } + + } + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { caller.renameTag(oldName: folder.name ?? "", newName: name) { result in @@ -768,109 +807,6 @@ private extension FeedbinAccountDelegate { } } - - func importOPMLItems(_ account: Account, items: [RSOPMLItem], parentFolder: Folder?) { - - items.forEach { (item) in - - if let feedSpecifier = item.feedSpecifier { - importFeedSpecifier(account, feedSpecifier: feedSpecifier, parentFolder: parentFolder) - return - } - - guard let folderName = item.titleFromAttributes else { - // Folder doesn’t have a name, so it won’t be created, and its items will go one level up. - if let itemChildren = item.children { - importOPMLItems(account, items: itemChildren, parentFolder: parentFolder) - } - return - } - - if let folder = account.ensureFolder(with: folderName) { - if let itemChildren = item.children { - importOPMLItems(account, items: itemChildren, parentFolder: folder) - } - } - - } - - } - - func importFeedSpecifier(_ account: Account, feedSpecifier: RSOPMLFeedSpecifier, parentFolder: Folder?) { - - caller.createSubscription(url: feedSpecifier.feedURL) { [weak self] result in - - switch result { - case .success(let subResult): - switch subResult { - case .created(let sub): - - DispatchQueue.main.async { - - let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL) - feed.subscriptionID = String(sub.subscriptionID) - - self?.importFeedSpecifierPostProcess(account: account, sub: sub, feedSpecifier: feedSpecifier, feed: feed, parentFolder: parentFolder) - - } - - default: - break - } - - case .failure(let error): - guard let self = self else { return } - os_log(.error, log: self.log, "Create feed on OPML import failed: %@.", error.localizedDescription) - } - - } - - } - - func importFeedSpecifierPostProcess(account: Account, sub: FeedbinSubscription, feedSpecifier: RSOPMLFeedSpecifier, feed: Feed, parentFolder: Folder?) { - - // Rename the feed if its name in the OPML file doesn't match the found name - if sub.name != feedSpecifier.title, let newName = feedSpecifier.title { - - self.caller.renameSubscription(subscriptionID: String(sub.subscriptionID), newName: newName) { [weak self] result in - switch result { - case .success: - DispatchQueue.main.async { - feed.editedName = newName - } - case .failure(let error): - guard let self = self else { return } - os_log(.error, log: self.log, "Rename feed on OPML import failed: %@.", error.localizedDescription) - } - } - - } - - // Move the new feed if it is in a folder - if let folder = parentFolder, let feedID = Int(feed.feedID) { - - self.caller.createTagging(feedID: feedID, name: folder.name ?? "") { [weak self] result in - switch result { - case .success(let taggingID): - DispatchQueue.main.async { - self?.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID)) - folder.addFeed(feed) - } - case .failure(let error): - guard let self = self else { return } - os_log(.error, log: self.log, "Move feed to folder on OPML import failed: %@.", error.localizedDescription) - } - } - - } else { - - DispatchQueue.main.async { - account.addFeed(feed) - } - - } - - } func processRestoredFeed(for account: Account, feed: Feed, editedName: String?, folder: Folder?, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Feedbin/FeedbinImportResult.swift b/Frameworks/Account/Feedbin/FeedbinImportResult.swift new file mode 100644 index 000000000..bce437960 --- /dev/null +++ b/Frameworks/Account/Feedbin/FeedbinImportResult.swift @@ -0,0 +1,21 @@ +// +// FeedbinImportResult.swift +// Account +// +// Created by Maurice Parker on 5/17/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct FeedbinImportResult: Codable { + + let importResultID: Int + let complete: Bool + + enum CodingKeys: String, CodingKey { + case importResultID = "id" + case complete + } + +}