diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 7bd076a2d..7a9531e4f 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -140,6 +140,8 @@ 9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */; }; 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; }; 9EA643CF2391D3560018A28C /* FeedlyAddExistingFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643CE2391D3550018A28C /* FeedlyAddExistingFeedOperation.swift */; }; + 9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */; }; + 9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */; }; 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; }; 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; }; 9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */; }; @@ -360,6 +362,8 @@ 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAccountAuthorizationOperation.swift; sourceTree = ""; }; 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = ""; }; 9EA643CE2391D3550018A28C /* FeedlyAddExistingFeedOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAddExistingFeedOperation.swift; sourceTree = ""; }; + 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySearchOperation.swift; sourceTree = ""; }; + 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedsSearchResponse.swift; sourceTree = ""; }; 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = ""; }; 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = ""; }; 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntry.swift; sourceTree = ""; }; @@ -697,6 +701,7 @@ children = ( 9E1D1554233431A600F4944C /* FeedlyOperation.swift */, 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */, + 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */, 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */, 9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */, 9EA643CE2391D3550018A28C /* FeedlyAddExistingFeedOperation.swift */, @@ -737,6 +742,7 @@ 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */, 9E1773D22345700E0056A5A8 /* FeedlyLink.swift */, 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */, + 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */, ); path = Models; sourceTree = ""; @@ -974,6 +980,7 @@ 514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */, 9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */, 8469F81C1F6DD15E0084783E /* Account.swift in Sources */, + 9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */, 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, 3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */, @@ -981,6 +988,7 @@ 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */, 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */, 846E77451F6EF9B900A165E2 /* Container.swift in Sources */, + 9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */, 9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */, 9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */, 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift index 408e093db..92eb0279f 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift @@ -32,13 +32,13 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { func testAddFeeds() { let feedsForFolderOne = [ - FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil), - FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil) + FeedlyFeed(id: "feed/1", title: "Feed One", updated: nil, website: nil), + FeedlyFeed(id: "feed/2", title: "Feed Two", updated: nil, website: nil) ] let feedsForFolderTwo = [ - FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil), - FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil), + FeedlyFeed(id: "feed/1", title: "Feed One", updated: nil, website: nil), + FeedlyFeed(id: "feed/3", title: "Feed Three", updated: nil, website: nil), ] let folderOne: (name: String, id: String) = ("FolderOne", "folder/1") @@ -66,7 +66,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { let feedIds = Set([feedsForFolderOne, feedsForFolderTwo] .flatMap { $0 } - .map { $0.feedId }) + .map { $0.id }) let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo] .flatMap { $0 } @@ -85,7 +85,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { let expectedFolderAndFeedIds = namesAndFeeds .sorted { $0.0.id < $1.0.id } .map { folder, feeds -> [String: [String]] in - return [folder.id: feeds.map { $0.feedId }.sorted(by: <)] + return [folder.id: feeds.map { $0.id }.sorted(by: <)] } let ingestedFolderAndFeedIds = (account.folders ?? Set()) @@ -100,16 +100,16 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { func testRemoveFeeds() { let folderOne: (name: String, id: String) = ("FolderOne", "folder/1") let folderTwo: (name: String, id: String) = ("FolderTwo", "folder/2") - let feedToRemove = FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil) + let feedToRemove = FeedlyFeed(id: "feed/1", title: "Feed One", updated: nil, website: nil) var feedsForFolderOne = [ feedToRemove, - FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil) + FeedlyFeed(id: "feed/2", title: "Feed Two", updated: nil, website: nil) ] var feedsForFolderTwo = [ feedToRemove, - FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil), + FeedlyFeed(id: "feed/3", title: "Feed Three", updated: nil, website: nil), ] // Add initial content. @@ -159,7 +159,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { let feedIds = Set([feedsForFolderOne, feedsForFolderTwo] .flatMap { $0 } - .map { $0.feedId }) + .map { $0.id }) let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo] .flatMap { $0 } @@ -181,7 +181,7 @@ class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { let expectedFolderAndFeedIds = namesAndFeeds .sorted { $0.0.id < $1.0.id } .map { folder, feeds -> [String: [String]] in - return [folder.id: feeds.map { $0.feedId }.sorted(by: <)] + return [folder.id: feeds.map { $0.id }.sorted(by: <)] } let ingestedFolderAndFeedIds = (account.folders ?? Set()) diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyMirrorCollectionsAsFoldersOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyMirrorCollectionsAsFoldersOperationTests.swift index ae6dd5dec..9ffff7a72 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlyMirrorCollectionsAsFoldersOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyMirrorCollectionsAsFoldersOperationTests.swift @@ -110,13 +110,13 @@ class FeedlyMirrorCollectionsAsFoldersOperationTests: XCTestCase { class CollectionsAndFeedsProvider: FeedlyCollectionProviding { var feedsForCollectionOne = [ - FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil), - FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil) + FeedlyFeed(id: "feed/1", title: "Feed One", updated: nil, website: nil), + FeedlyFeed(id: "feed/2", title: "Feed Two", updated: nil, website: nil) ] var feedsForCollectionTwo = [ - FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil), - FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil), + FeedlyFeed(id: "feed/1", title: "Feed One", updated: nil, website: nil), + FeedlyFeed(id: "feed/3", title: "Feed Three", updated: nil, website: nil), ] var collections: [FeedlyCollection] { diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index 10f58b59a..a6b7b4053 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -226,6 +226,55 @@ final class FeedlyAPICaller { } } + func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completionHandler: @escaping (Result) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + + guard let encodedCollectionId = encodeForURLPath(collectionId) else { + return DispatchQueue.main.async { + completionHandler(.failure(FeedlyAccountDelegateError.unexpectedResourceId(collectionId))) + } + } + + guard let encodedFeedId = encodeForURLPath(feedId) else { + return DispatchQueue.main.async { + completionHandler(.failure(FeedlyAccountDelegateError.unexpectedResourceId(feedId))) + } + } + + var components = baseUrlComponents + components.percentEncodedPath = "/v3/collections/\(encodedCollectionId)/feeds/\(encodedFeedId)" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let httpResponse, _): + if httpResponse.statusCode == 200 { + completionHandler(.success(())) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } +} + +extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { + func addFeed(with feedId: FeedlyFeedResourceId, title: String? = nil, toCollectionWith collectionId: String, completionHandler: @escaping (Result<[FeedlyFeed], Error>) -> ()) { guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { @@ -278,52 +327,6 @@ final class FeedlyAPICaller { } } } - - func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completionHandler: @escaping (Result) -> ()) { - guard let accessToken = credentials?.secret else { - return DispatchQueue.main.async { - completionHandler(.failure(CredentialsError.incompleteCredentials)) - } - } - - guard let encodedCollectionId = encodeForURLPath(collectionId) else { - return DispatchQueue.main.async { - completionHandler(.failure(FeedlyAccountDelegateError.unexpectedResourceId(collectionId))) - } - } - - guard let encodedFeedId = encodeForURLPath(feedId) else { - return DispatchQueue.main.async { - completionHandler(.failure(FeedlyAccountDelegateError.unexpectedResourceId(feedId))) - } - } - - var components = baseUrlComponents - components.percentEncodedPath = "/v3/collections/\(encodedCollectionId)/feeds/\(encodedFeedId)" - - guard let url = components.url else { - fatalError("\(components) does not produce a valid URL.") - } - - var request = URLRequest(url: url) - request.httpMethod = "DELETE" - request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) - request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - - transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in - switch result { - case .success(let httpResponse, _): - if httpResponse.statusCode == 200 { - completionHandler(.success(())) - } else { - completionHandler(.failure(URLError(.cannotDecodeContentData))) - } - case .failure(let error): - completionHandler(.failure(error)) - } - } - } } extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { @@ -688,6 +691,44 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService { } } +extension FeedlyAPICaller: FeedlySearchService { + + func getFeeds(for query: String, count: Int, locale: String, completionHandler: @escaping (Result) -> ()) { + + var components = baseUrlComponents + components.path = "/v3/search/feeds" + + components.queryItems = [ + URLQueryItem(name: "query", value: query), + URLQueryItem(name: "count", value: String(count)), + URLQueryItem(name: "locale", value: locale) + ] + + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + + transport.send(request: request, resultType: FeedlyFeedsSearchResponse.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let (_, searchResponse)): + if let response = searchResponse { + completionHandler(.success(response)) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } +} + extension FeedlyAPICaller: FeedlyLogoutService { func logout(completionHandler: @escaping (Result) -> ()) { diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 5cff3405e..34d992b85 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -292,12 +292,14 @@ final class FeedlyAccountDelegate: AccountDelegate { throw FeedlyAccountDelegateError.notLoggedIn } - let resource = FeedlyFeedResourceId(url: url) let addNewFeed = try FeedlyAddNewFeedOperation(account: account, credentials: credentials, - resource: resource, + url: url, feedName: name, - caller: caller, + searchService: caller, + addToCollectionService: caller, + syncUnreadIdsService: caller, + getStreamContentsService: caller, container: container, progress: refreshProgress, log: log) @@ -353,7 +355,7 @@ final class FeedlyAccountDelegate: AccountDelegate { let addExistingFeed = try FeedlyAddExistingFeedOperation(account: account, credentials: credentials, resource: resource, - caller: caller, + service: caller, container: container, progress: refreshProgress, log: log) diff --git a/Frameworks/Account/Feedly/Models/FeedlyFeed.swift b/Frameworks/Account/Feedly/Models/FeedlyFeed.swift index 0f48fa83b..438365110 100644 --- a/Frameworks/Account/Feedly/Models/FeedlyFeed.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyFeed.swift @@ -9,7 +9,6 @@ import Foundation struct FeedlyFeed: Codable { - var feedId: String var id: String var title: String? var updated: Date? diff --git a/Frameworks/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift b/Frameworks/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift new file mode 100644 index 000000000..17437ac23 --- /dev/null +++ b/Frameworks/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift @@ -0,0 +1,19 @@ +// +// FeedlyFeedsSearchResponse.swift +// Account +// +// Created by Kiel Gillard on 1/12/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct FeedlyFeedsSearchResponse: Decodable { + + struct Feed: Decodable { + var title: String + var feedId: String + } + + var results: [Feed] +} diff --git a/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift b/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift index 7b3812eba..8d4bb9f36 100644 --- a/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift @@ -22,6 +22,10 @@ struct FeedlyFeedResourceId: FeedlyResourceId { /// The location of the kind of resource a concrete type represents. /// If the conrete type cannot strip the resource type from the Id, it should just return the Id /// since the Id is a legitimate URL. + /// This is basically assuming Feedly prefixes source feed URLs with `feed/`. + /// It is not documented as such and could potentially change. + /// Feedly does not include the source feed URL as a separate field. + /// See https://developer.feedly.com/v3/feeds/#get-the-metadata-about-a-specific-feed var url: String { if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex { var mutant = id diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift index a3b6e3739..0aab065e3 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift @@ -15,7 +15,7 @@ class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, var addCompletionHandler: ((Result) -> ())? - init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceId, caller: FeedlyAPICaller, container: Container, progress: DownloadProgress, log: OSLog) throws { + init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceId, service: FeedlyAddFeedToCollectionService, container: Container, progress: DownloadProgress, log: OSLog) throws { let validator = FeedlyFeedContainerValidator(container: container, userId: credentials.username) let (folder, collectionId) = try validator.getValidContainer() @@ -27,7 +27,7 @@ class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, self.downloadProgress = progress - let addRequest = FeedlyAddFeedToCollectionOperation(account: account, folder: folder, feedResource: resource, feedName: nil, collectionId: collectionId, caller: caller) + let addRequest = FeedlyAddFeedToCollectionOperation(account: account, folder: folder, feedResource: resource, feedName: nil, collectionId: collectionId, service: service) addRequest.delegate = self addRequest.downloadProgress = progress self.operationQueue.addOperation(addRequest) diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift index 1ac8f6c81..eec23c32b 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift @@ -8,21 +8,25 @@ import Foundation +protocol FeedlyAddFeedToCollectionService { + func addFeed(with feedId: FeedlyFeedResourceId, title: String?, toCollectionWith collectionId: String, completionHandler: @escaping (Result<[FeedlyFeed], Error>) -> ()) +} + final class FeedlyAddFeedToCollectionOperation: FeedlyOperation, FeedlyFeedsAndFoldersProviding, FeedlyResourceProviding { let feedName: String? let collectionId: String - let caller: FeedlyAPICaller + let service: FeedlyAddFeedToCollectionService let account: Account let folder: Folder let feedResource: FeedlyFeedResourceId - init(account: Account, folder: Folder, feedResource: FeedlyFeedResourceId, feedName: String? = nil, collectionId: String, caller: FeedlyAPICaller) { + init(account: Account, folder: Folder, feedResource: FeedlyFeedResourceId, feedName: String? = nil, collectionId: String, service: FeedlyAddFeedToCollectionService) { self.account = account self.folder = folder self.feedResource = feedResource self.feedName = feedName self.collectionId = collectionId - self.caller = caller + self.service = service } private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]() @@ -36,7 +40,7 @@ final class FeedlyAddFeedToCollectionOperation: FeedlyOperation, FeedlyFeedsAndF return didFinish() } - caller.addFeed(with: feedResource, title: feedName, toCollectionWith: collectionId) { [weak self] result in + service.addFeed(with: feedResource, title: feedName, toCollectionWith: collectionId) { [weak self] result in guard let self = self else { return } @@ -52,7 +56,7 @@ final class FeedlyAddFeedToCollectionOperation: FeedlyOperation, FeedlyFeedsAndF case .success(let feedlyFeeds): feedsAndFolders = [(feedlyFeeds, folder)] - let feedsWithCreatedFeedId = feedlyFeeds.filter { $0.feedId == resource.id } + let feedsWithCreatedFeedId = feedlyFeeds.filter { $0.id == resource.id } if feedsWithCreatedFeedId.isEmpty { didFinish(AccountError.createErrorNotFound) diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift index bd15ede20..db56fc0c4 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -10,52 +10,57 @@ import Foundation import os.log import RSWeb -class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate { +protocol FeedlyFeedResourceIdProviding { + var feedResourceId: String { get } +} + +extension FeedlyFeedResourceId: FeedlyFeedResourceIdProviding { + + var feedResourceId: String { + return id + } +} + +class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate { private let operationQueue: OperationQueue private let folder: Folder - private let feedResourceId: FeedlyFeedResourceId + private let collectionId: String + private let url: String + private let account: Account + private let credentials: Credentials + private let feedName: String? + private let addToCollectionService: FeedlyAddFeedToCollectionService + private let syncUnreadIdsService: FeedlyGetStreamIdsService + private let getStreamContentsService: FeedlyGetStreamContentsService + private let log: OSLog var addCompletionHandler: ((Result) -> ())? - init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceId, feedName: String?, caller: FeedlyAPICaller, container: Container, progress: DownloadProgress, log: OSLog) throws { + init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, container: Container, progress: DownloadProgress, log: OSLog) throws { let validator = FeedlyFeedContainerValidator(container: container, userId: credentials.username) - let (folder, collectionId) = try validator.getValidContainer() + (self.folder, self.collectionId) = try validator.getValidContainer() - self.folder = folder - self.feedResourceId = resource + self.url = url self.operationQueue = OperationQueue() self.operationQueue.isSuspended = true + self.account = account + self.credentials = credentials + self.feedName = feedName + self.addToCollectionService = addToCollectionService + self.syncUnreadIdsService = syncUnreadIdsService + self.getStreamContentsService = getStreamContentsService + self.log = log super.init() self.downloadProgress = progress - let addRequest = FeedlyAddFeedToCollectionOperation(account: account, folder: folder, feedResource: resource, feedName: feedName, collectionId: collectionId, caller: caller) - addRequest.delegate = self - addRequest.downloadProgress = progress - self.operationQueue.addOperation(addRequest) - - let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) - createFeeds.addDependency(addRequest) - createFeeds.downloadProgress = progress - self.operationQueue.addOperation(createFeeds) - - let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log) - syncUnread.addDependency(addRequest) - syncUnread.downloadProgress = progress - self.operationQueue.addOperation(syncUnread) - - let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: caller, newerThan: nil, log: log) - syncFeed.addDependency(syncUnread) - syncFeed.downloadProgress = progress - self.operationQueue.addOperation(syncFeed) - - let finishOperation = FeedlyCheckpointOperation() - finishOperation.checkpointDelegate = self - finishOperation.downloadProgress = progress - finishOperation.addDependency(syncFeed) - self.operationQueue.addOperation(finishOperation) + let search = FeedlySearchOperation(query: url, locale: .current, service: searchService) + search.delegate = self + search.searchDelegate = self + search.downloadProgress = progress + self.operationQueue.addOperation(search) } override func cancel() { @@ -71,6 +76,46 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl operationQueue.isSuspended = false } + private var feedResourceId: FeedlyFeedResourceId? + + func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) { + guard !isCancelled else { + return + } + guard let first = response.results.first else { + return didFinish(AccountError.createErrorNotFound) + } + + let feedResourceId = FeedlyFeedResourceId(id: first.feedId) + self.feedResourceId = feedResourceId + + let addRequest = FeedlyAddFeedToCollectionOperation(account: account, folder: folder, feedResource: feedResourceId, feedName: feedName, collectionId: collectionId, service: addToCollectionService) + addRequest.delegate = self + addRequest.downloadProgress = downloadProgress + self.operationQueue.addOperation(addRequest) + + let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) + createFeeds.addDependency(addRequest) + createFeeds.downloadProgress = downloadProgress + self.operationQueue.addOperation(createFeeds) + + let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log) + syncUnread.addDependency(addRequest) + syncUnread.downloadProgress = downloadProgress + self.operationQueue.addOperation(syncUnread) + + let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: feedResourceId, service: getStreamContentsService, newerThan: nil, log: log) + syncFeed.addDependency(syncUnread) + syncFeed.downloadProgress = downloadProgress + self.operationQueue.addOperation(syncFeed) + + let finishOperation = FeedlyCheckpointOperation() + finishOperation.checkpointDelegate = self + finishOperation.downloadProgress = downloadProgress + finishOperation.addDependency(syncFeed) + self.operationQueue.addOperation(finishOperation) + } + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { addCompletionHandler?(.failure(error)) addCompletionHandler = nil @@ -91,7 +136,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl return } - if let feed = folder.existingWebFeed(withWebFeedID: feedResourceId.id) { + if let feedResource = feedResourceId, let feed = folder.existingWebFeed(withWebFeedID: feedResource.id) { handler(.success(feed)) } else { diff --git a/Frameworks/Account/Feedly/Operations/FeedlySearchOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlySearchOperation.swift new file mode 100644 index 000000000..e9b712496 --- /dev/null +++ b/Frameworks/Account/Feedly/Operations/FeedlySearchOperation.swift @@ -0,0 +1,52 @@ +// +// FeedlySearchOperation.swift +// Account +// +// Created by Kiel Gillard on 1/12/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +protocol FeedlySearchService: class { + func getFeeds(for query: String, count: Int, locale: String, completionHandler: @escaping (Result) -> ()) +} + +protocol FeedlySearchOperationDelegate: class { + func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) +} + +/// Single responsibility is to find one and only one feed for a given query (usually, a URL). +/// What happens when a feed is found for the URL is delegated to the `searchDelegate`. +class FeedlySearchOperation: FeedlyOperation { + let query: String + let locale: Locale + let searchService: FeedlySearchService + + weak var searchDelegate: FeedlySearchOperationDelegate? + + init(query: String, locale: Locale = .current, service: FeedlySearchService) { + self.query = query + self.locale = locale + self.searchService = service + } + + override func main() { + guard !isCancelled else { + didFinish() + return + } + + searchService.getFeeds(for: query, count: 1, locale: locale.identifier) { result in + switch result { + case .success(let response): + assert(Thread.isMainThread) + self.searchDelegate?.feedlySearchOperation(self, didGet: response) + self.didFinish() + + case .failure(let error): + self.didFinish(error) + } + } + } +}