Add folder syncing for Feedbin account

This commit is contained in:
Maurice Parker
2019-05-05 15:41:20 -05:00
parent 29f9cf83b1
commit 15a0ba89d7
18 changed files with 435 additions and 137 deletions

View File

@@ -281,9 +281,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
}
public func refreshAll() {
delegate.refreshAll(for: self)
public func refreshAll(completionHandler completion: (() -> Void)? = nil) {
delegate.refreshAll(for: self, completionHandler: completion)
}
public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) {

View File

@@ -9,9 +9,14 @@
/* Begin PBXBuildFile section */
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; };
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
5107A09D227DE77700C7C3C5 /* NilTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* NilTransport.swift */; };
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; };
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; };
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
51D5875C227F630B00900287 /* tags_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58759227F630B00900287 /* tags_initial.json */; };
51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */; };
841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; };
841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; };
841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; };
@@ -28,7 +33,6 @@
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; };
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846E77531F6F00E300A165E2 /* AccountManager.swift */; };
848935001F62484F00CEBD24 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848934F61F62484F00CEBD24 /* Account.framework */; };
848935051F62485000CEBD24 /* AccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848935041F62485000CEBD24 /* AccountTests.swift */; };
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */; };
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */; };
84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */; };
@@ -90,9 +94,14 @@
/* Begin PBXFileReference section */
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = "<group>"; };
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
5107A09C227DE77700C7C3C5 /* NilTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NilTransport.swift; sourceTree = "<group>"; };
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = "<group>"; };
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
51D58759227F630B00900287 /* tags_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_initial.json; sourceTree = "<group>"; };
51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderSyncTest.swift; sourceTree = "<group>"; };
841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = "<group>"; };
@@ -110,7 +119,6 @@
848934F61F62484F00CEBD24 /* Account.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Account.framework; sourceTree = BUILT_PRODUCTS_DIR; };
848934FA1F62484F00CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
848934FF1F62484F00CEBD24 /* AccountTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AccountTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
848935041F62485000CEBD24 /* AccountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTests.swift; sourceTree = "<group>"; };
848935061F62485000CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
848935101F62486800CEBD24 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadata.swift; sourceTree = "<group>"; };
@@ -156,6 +164,16 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
51D58756227F62E300900287 /* JSON */ = {
isa = PBXGroup;
children = (
51D58758227F630B00900287 /* tags_add.json */,
51D58757227F630B00900287 /* tags_delete.json */,
51D58759227F630B00900287 /* tags_initial.json */,
);
path = JSON;
sourceTree = "<group>";
};
841973E91F6DD19E006346C4 /* Products */ = {
isa = PBXGroup;
children = (
@@ -189,11 +207,12 @@
children = (
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */,
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */,
84245C841FDDD8CB0074AFBB /* FeedbinFeed.swift */,
84CAD7151FDF2E22000F0755 /* FeedbinArticle.swift */,
84D096202174169100D77525 /* FeedbinArticleIDArray.swift */,
84D09622217418DC00D77525 /* FeedbinTagging.swift */,
84245C841FDDD8CB0074AFBB /* FeedbinFeed.swift */,
84D0962421741B8500D77525 /* FeedbinSavedSearch.swift */,
51D58754227F53BE00900287 /* FeedbinTag.swift */,
84D09622217418DC00D77525 /* FeedbinTagging.swift */,
);
path = Feedbin;
sourceTree = "<group>";
@@ -250,11 +269,12 @@
848935031F62484F00CEBD24 /* AccountTests */ = {
isa = PBXGroup;
children = (
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */,
848935041F62485000CEBD24 /* AccountTests.swift */,
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */,
51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */,
5107A09C227DE77700C7C3C5 /* TestTransport.swift */,
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */,
51D58756227F62E300900287 /* JSON */,
848935061F62485000CEBD24 /* Info.plist */,
5107A09C227DE77700C7C3C5 /* NilTransport.swift */,
);
path = AccountTests;
sourceTree = "<group>";
@@ -419,6 +439,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
51D5875B227F630B00900287 /* tags_add.json in Resources */,
51D5875C227F630B00900287 /* tags_initial.json in Resources */,
51D5875A227F630B00900287 /* tags_delete.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -443,6 +466,7 @@
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
84CAD7161FDF2E22000F0755 /* FeedbinArticle.swift in Sources */,
84D0962521741B8500D77525 /* FeedbinSavedSearch.swift in Sources */,
@@ -458,10 +482,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */,
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
5107A09D227DE77700C7C3C5 /* NilTransport.swift in Sources */,
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */,
848935051F62485000CEBD24 /* AccountTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -19,7 +19,7 @@ protocol AccountDelegate {
var refreshProgress: DownloadProgress { get }
func refreshAll(for: Account)
func refreshAll(for: Account, completionHandler completion: (() -> Void)?)
// Called at the end of accounts init method.

View File

@@ -17,6 +17,8 @@ final class AccountMetadata: Codable {
struct ConditionalGetKeys {
static let subscriptions = "subscriptions"
static let tags = "tags"
static let taggings = "taggings"
}
enum CodingKeys: String, CodingKey {

View File

@@ -15,7 +15,7 @@ class AccountCredentialsTest: XCTestCase {
private var account: Account!
override func setUp() {
account = TestAccountManager.shared.createAccount(type: .feedbin, transport: NilTransport())
account = TestAccountManager.shared.createAccount(type: .feedbin, transport: TestTransport())
}
override func tearDown() {

View File

@@ -0,0 +1,83 @@
//
// AccountFolderSyncTest.swift
// AccountTests
//
// Created by Maurice Parker on 5/5/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class AccountFolderSyncTest: XCTestCase {
override func setUp() {
}
override func tearDown() {
}
func testFolderSync() {
let testTransport = TestTransport()
testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_initial.json"
let account = TestAccountManager.shared.createAccount(type: .feedbin, transport: testTransport)
// Test initial folders
let initialExpection = self.expectation(description: "Initial tags")
account.refreshAll() {
initialExpection.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
guard let intialFolders = account.folders else {
XCTFail()
return
}
XCTAssertEqual(9, intialFolders.count)
let initialFolderNames = intialFolders.map { $0.name ?? "" }
XCTAssertTrue(initialFolderNames.contains("Outdoors"))
// Test removing folders
testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_delete.json"
let deleteExpection = self.expectation(description: "Delete tags")
account.refreshAll() {
deleteExpection.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
guard let deleteFolders = account.folders else {
XCTFail()
return
}
XCTAssertEqual(8, deleteFolders.count)
let deleteFolderNames = deleteFolders.map { $0.name ?? "" }
XCTAssertTrue(deleteFolderNames.contains("Outdoors"))
XCTAssertFalse(deleteFolderNames.contains("Tech Media"))
// Test Adding Folders
testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_add.json"
let addExpection = self.expectation(description: "Add tags")
account.refreshAll() {
addExpection.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
guard let addFolders = account.folders else {
XCTFail()
return
}
XCTAssertEqual(10, addFolders.count)
let addFolderNames = addFolders.map { $0.name ?? "" }
XCTAssertTrue(addFolderNames.contains("Vanlife"))
TestAccountManager.shared.deleteAccount(account)
}
}

View File

@@ -1,36 +0,0 @@
//
// AccountTests.swift
// AccountTests
//
// Created by Brent Simmons on 9/7/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class AccountTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

View File

@@ -0,0 +1,42 @@
[
{
"id": 9754,
"name": "Amusement"
},
{
"id": 247,
"name": "Business"
},
{
"id": 1049,
"name": "Developers"
},
{
"id": 102244,
"name": "Development Orgs"
},
{
"id": 40541,
"name": "Open Web"
},
{
"id": 1337,
"name": "Outdoors"
},
{
"id": 56975,
"name": "Overlanding"
},
{
"id": 3782,
"name": "Pundits"
},
{
"id": 7827,
"name": "Tech Media"
},
{
"id": 97769,
"name": "Vanlife"
}
]

View File

@@ -0,0 +1,34 @@
[
{
"id": 9754,
"name": "Amusement"
},
{
"id": 247,
"name": "Business"
},
{
"id": 1049,
"name": "Developers"
},
{
"id": 102244,
"name": "Development Orgs"
},
{
"id": 40541,
"name": "Open Web"
},
{
"id": 1337,
"name": "Outdoors"
},
{
"id": 56975,
"name": "Overlanding"
},
{
"id": 3782,
"name": "Pundits"
}
]

View File

@@ -0,0 +1,38 @@
[
{
"id": 9754,
"name": "Amusement"
},
{
"id": 247,
"name": "Business"
},
{
"id": 1049,
"name": "Developers"
},
{
"id": 102244,
"name": "Development Orgs"
},
{
"id": 40541,
"name": "Open Web"
},
{
"id": 1337,
"name": "Outdoors"
},
{
"id": 56975,
"name": "Overlanding"
},
{
"id": 3782,
"name": "Pundits"
},
{
"id": 7827,
"name": "Tech Media"
}
]

View File

@@ -1,21 +0,0 @@
//
// NilTransport.swift
// AccountTests
//
// Created by Maurice Parker on 5/4/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSWeb
struct NilTransport: Transport {
func send<T>(request: URLRequest, resultType: T.Type, completion: @escaping (Result<(HTTPHeaders, T), Error>) -> Void) where T : Decodable, T : Encodable {
}
func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data), Error>) -> Void) {
}
}

View File

@@ -0,0 +1,34 @@
//
// TestTransport.swift
// AccountTests
//
// Created by Maurice Parker on 5/4/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSWeb
final class TestTransport: Transport {
enum TestTransportError: String, Error {
case invalidState = "The test wasn't set up correctly."
}
var testFiles = [String: String]()
func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data), Error>) -> Void) {
guard let urlString = request.url?.absoluteString else {
completion(.failure(TestTransportError.invalidState))
return
}
let testFileName = testFiles[urlString]!
let testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testFileName)
let data = try! Data(contentsOf: testFileURL)
completion(.success((HTTPHeaders(), data)))
}
}

View File

@@ -47,6 +47,47 @@ final class FeedbinAPICaller: NSObject {
}
func retrieveTags(completionHandler completion: @escaping (Result<[FeedbinTag], Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("tags.json")
let conditionalGet = accountMetadata?.conditionalGetInfo[AccountMetadata.ConditionalGetKeys.tags]
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
transport.send(request: request, resultType: [FeedbinTag].self) { [weak self] result in
switch result {
case .success(let (headers, tags)):
self?.storeConditionalGet(metadata: self?.accountMetadata, key: AccountMetadata.ConditionalGetKeys.tags, headers: headers)
completion(.success(tags))
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveTaggings(completionHandler completion: @escaping (Result<[FeedbinTagging], Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("taggings.json")
let conditionalGet = accountMetadata?.conditionalGetInfo[AccountMetadata.ConditionalGetKeys.taggings]
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
transport.send(request: request, resultType: [FeedbinTagging].self) { [weak self] result in
switch result {
case .success(let (headers, taggings)):
self?.storeConditionalGet(metadata: self?.accountMetadata, key: AccountMetadata.ConditionalGetKeys.taggings, headers: headers)
// TODO: Add paging code
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveSubscriptions(completionHandler completion: @escaping (Result<[FeedbinFeed], Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")

View File

@@ -6,7 +6,12 @@
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
#if os(macOS)
import AppKit
#else
import UIKit
import RSCore
#endif
import RSWeb
final class FeedbinAccountDelegate: AccountDelegate {
@@ -33,18 +38,32 @@ final class FeedbinAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0)
static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result<Bool, Error>) -> Void) {
static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler completion: @escaping (Result<Bool, Error>) -> Void) {
let caller = FeedbinAPICaller(transport: transport)
caller.credentials = credentials
caller.validateCredentials() { result in
handler(result)
completion(result)
}
}
func refreshAll(for account: Account) {
func refreshAll(for account: Account, completionHandler completion: (() -> Void)? = nil) {
refreshAll(account) { result in
switch result {
case .success():
completion?()
case .failure(let error):
// TODO: We should do a better job of error handling here.
// We need to prompt for credentials and provide user friendly
// errors.
#if os(macOS)
NSApplication.shared.presentError(error)
#else
UIApplication.shared.presentError(error)
#endif
}
}
}
func accountDidInitialize(_ account: Account) {
@@ -52,3 +71,67 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
}
// MARK: Private
private extension FeedbinAccountDelegate {
func refreshAll(_ account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
caller.retrieveTags { [weak self] result in
switch result {
case .success(let tags):
self?.syncFolders(account, tags)
completion(.success(()))
case .failure(let error):
self?.checkErrorOrNotModified(error, completion: completion)
}
}
}
func syncFolders(_ account: Account, _ tags: [FeedbinTag]) {
let tagNames = tags.map { $0.name }
// Delete any folders not at Feedbin
if let folders = account.folders {
folders.forEach { folder in
if !tagNames.contains(folder.name ?? "") {
account.deleteFolder(folder)
}
}
}
let folderNames: [String] = {
if let folders = account.folders {
return folders.map { $0.name ?? "" }
} else {
return [String]()
}
}()
// Make any folders Feedbin has, but we don't
tagNames.forEach { tagName in
if !folderNames.contains(tagName) {
account.ensureFolder(with: tagName)
}
}
}
func checkErrorOrNotModified(_ error: Error, completion: @escaping (Result<Void, Error>) -> Void) {
switch error {
case TransportError.httpError(let status):
if status == HTTPResponseCode.notModified {
completion(.success(()))
} else {
completion(.failure(error))
}
default:
completion(.failure(error))
}
}
}

View File

@@ -0,0 +1,21 @@
//
// FeedbinTag.swift
// Account
//
// Created by Maurice Parker on 5/5/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct FeedbinTag: Codable, Equatable, Hashable {
let tagID: Int
let name: String
enum CodingKeys: String, CodingKey {
case tagID = "id"
case name = "name"
}
}

View File

@@ -8,68 +8,16 @@
import Foundation
struct FeedbinTagging: Hashable {
// https://github.com/feedbin/feedbin-api/blob/master/content/taggings.md
//
// [
// {
// "id": 4,
// "feed_id": 1,
// "name": "Tech"
// },
// {
// "id": 5,
// "feed_id": 2,
// "name": "News"
// }
// ]
struct FeedbinTagging: Codable, Equatable, Hashable {
let taggingID: Int
let feedID: Int
let name: String
private struct Key {
static let taggingID = "id"
static let feedID = "feed_id"
static let name = "name"
enum CodingKeys: String, CodingKey {
case taggingID = "id"
case feedID = "feed_id"
case name = "name"
}
init?(jsonDictionary: [String: Any]) {
guard let taggingID = jsonDictionary[Key.taggingID] as? Int else {
return nil
}
guard let feedID = jsonDictionary[Key.feedID] as? Int else {
return nil
}
guard let name = jsonDictionary[Key.name] as? String else {
return nil
}
self.taggingID = taggingID
self.feedID = feedID
self.name = name
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(taggingID)
}
// MARK: - Equatable
public static func ==(lhs: FeedbinTagging, rhs: FeedbinTagging) -> Bool {
return lhs.taggingID == rhs.taggingID && lhs.feedID == rhs.feedID && lhs.name == rhs.name
}
static func taggings(with jsonArray: [Any]) -> Set<FeedbinTagging> {
let taggingsArray = jsonArray.compactMap { (item) -> FeedbinTagging? in
if let oneDictionary = item as? [String: Any] {
return FeedbinTagging(jsonDictionary: oneDictionary)
}
return nil
}
return Set(taggingsArray)
}
}

View File

@@ -26,9 +26,10 @@ final class LocalAccountDelegate: AccountDelegate {
return handler(.success(false))
}
func refreshAll(for account: Account) {
// LocalAccountDelegate doesn't wait for completion before calling the completion block
func refreshAll(for account: Account, completionHandler completion: (() -> Void)? = nil) {
refresher.refreshFeeds(account.flattenedFeeds())
completion?()
}
func accountDidInitialize(_ account: Account) {

View File

@@ -78,13 +78,18 @@ class AccountsFeedbinWindowController: NSWindowController {
if authenticated {
var newAccount = false
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .feedbin)
newAccount = true
}
do {
try self.account?.removeBasicCredentials()
try self.account?.storeCredentials(credentials)
if newAccount {
self.account?.refreshAll()
}
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
} catch {
self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")