From 7563d1b2c681a02acc1b1d2ddbe6fb296d9f74f8 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Mon, 2 Dec 2019 07:39:05 +1100 Subject: [PATCH 1/3] Search Feedly for the best feed (and its identifier) for the URL entered when adding a new feed. #1300 --- .../Account/Account.xcodeproj/project.pbxproj | 8 ++ ...dsForCollectionFoldersOperationTests.swift | 22 +-- ...orCollectionsAsFoldersOperationTests.swift | 8 +- .../Account/Feedly/FeedlyAPICaller.swift | 133 ++++++++++++------ .../Feedly/FeedlyAccountDelegate.swift | 10 +- .../Account/Feedly/Models/FeedlyFeed.swift | 1 - .../Models/FeedlyFeedsSearchResponse.swift | 19 +++ .../Feedly/Models/FeedlyResourceId.swift | 4 + .../FeedlyAddExistingFeedOperation.swift | 4 +- .../FeedlyAddFeedToCollectionOperation.swift | 14 +- .../FeedlyAddNewFeedOperation.swift | 109 +++++++++----- .../Operations/FeedlySearchOperation.swift | 52 +++++++ 12 files changed, 279 insertions(+), 105 deletions(-) create mode 100644 Frameworks/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift create mode 100644 Frameworks/Account/Feedly/Operations/FeedlySearchOperation.swift 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) + } + } + } +} From d9a2ca8e7ecce9835fbca09417bd80869dec9f84 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Tue, 3 Dec 2019 19:09:10 +1100 Subject: [PATCH 2/3] Adds unit tests for adding a new feed with Feedly. #1300 --- .../Account/Account.xcodeproj/project.pbxproj | 10 +- .../FeedlyAddNewFeedOperationTests.swift | 334 ++++++++++++++++++ .../feedly-add-new-feed/collections.json | 1 + .../feedly-add-new-feed/emptyCollections.json | 1 + .../feedly-add-new-feed/feedStream.json | 1 + .../Feedly/feedly-add-new-feed/putFeed.json | 1 + .../feedly-add-new-feed/searchResults.json | 1 + .../Feedly/feedly-add-new-feed/unreadIds.json | 15 + .../Feedly/Models/FeedlyResourceId.swift | 7 + 9 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 Frameworks/Account/AccountTests/Feedly/FeedlyAddNewFeedOperationTests.swift create mode 100644 Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/collections.json create mode 100644 Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/emptyCollections.json create mode 100644 Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/feedStream.json create mode 100644 Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/putFeed.json create mode 100644 Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/searchResults.json create mode 100644 Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/unreadIds.json diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 7a9531e4f..bd6997e20 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -74,7 +74,6 @@ 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */; }; 844B2981210CE3BF004020B3 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844B2980210CE3BF004020B3 /* RSWeb.framework */; }; 8469F81C1F6DD15E0084783E /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848935101F62486800CEBD24 /* Account.swift */; }; - 846CA1882392349E00B55117 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846CA1872392349E00B55117 /* SyncDatabase.framework */; }; 846E77451F6EF9B900A165E2 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419740D1F6DD25F006346C4 /* Container.swift */; }; 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742C1F6DDE84006346C4 /* LocalAccountDelegate.swift */; }; 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; }; @@ -127,6 +126,7 @@ 9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */; }; 9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */; }; 9E784EC0237E8BE100099B1B /* FeedlyLogoutOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */; }; + 9E79F7742395C9F00031DB98 /* feedly-add-new-feed in Resources */ = {isa = PBXBuildFile; fileRef = 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */; }; 9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */; }; 9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */; }; 9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */; }; @@ -142,6 +142,7 @@ 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 */; }; + 9EA643D923945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.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 */; }; @@ -295,7 +296,6 @@ 844B297C2106C7EC004020B3 /* WebFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebFeed.swift; sourceTree = ""; }; 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProvider.swift; sourceTree = ""; }; 844B2980210CE3BF004020B3 /* RSWeb.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSWeb.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 846CA1872392349E00B55117 /* SyncDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SyncDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 846E77531F6F00E300A165E2 /* AccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; 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 = ""; }; @@ -349,6 +349,7 @@ 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceProviding.swift; sourceTree = ""; }; 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperation.swift; sourceTree = ""; }; 9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperationTests.swift; sourceTree = ""; }; + 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-add-new-feed"; sourceTree = ""; }; 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperationTests.swift; sourceTree = ""; }; 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperationTests.swift; sourceTree = ""; }; 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperation.swift; sourceTree = ""; }; @@ -364,6 +365,7 @@ 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 = ""; }; + 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperationTests.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 = ""; }; @@ -667,6 +669,8 @@ 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */, 9E0260CA236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift */, 9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */, + 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */, + 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */, 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */, 9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */, 9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */, @@ -933,6 +937,7 @@ 9EC804E5236C1A7F0057CFCB /* feedly-2-changestatuses in Resources */, 51D5875A227F630B00900287 /* tags_delete.json in Resources */, 9EC804E7236C1BA60057CFCB /* feedly-3-changestatusesagain in Resources */, + 9E79F7742395C9F00031DB98 /* feedly-add-new-feed in Resources */, 9EC804EF236C20DD0057CFCB /* feedly_macintosh_initial.json in Resources */, 5165D71722821C2400D9D53D /* taggings_add.json in Resources */, 5165D71622821C2400D9D53D /* taggings_delete.json in Resources */, @@ -1096,6 +1101,7 @@ 9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */, 9E0260CB236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift in Sources */, 9E1FF8622368219B00834C24 /* TestGetPagedStreamIdsService.swift in Sources */, + 9EA643D923945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift in Sources */, 9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */, 9E03C11E235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift in Sources */, 9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */, diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyAddNewFeedOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyAddNewFeedOperationTests.swift new file mode 100644 index 000000000..20f6e1a69 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyAddNewFeedOperationTests.swift @@ -0,0 +1,334 @@ +// +// FeedlyAddNewFeedOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 2/12/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account +import RSWeb + +class FeedlyAddNewFeedOperationTests: XCTestCase { + + private var account: Account! + private let support = FeedlyTestSupport() + + override func setUp() { + super.setUp() + account = support.makeTestAccount() + } + + override func tearDown() { + if let account = account { + support.destroy(account) + } + super.tearDown() + } + + private var transport = TestTransport() + lazy var caller: FeedlyAPICaller = { + let caller = FeedlyAPICaller(transport: transport, api: .sandbox) + caller.credentials = support.accessToken + return caller + }() + + private func getFolderByLoadingInitialContent() -> Folder? { + let subdirectory = "feedly-add-new-feed" + let provider = InitialMockResponseProvider(findingMocksIn: subdirectory) + + transport.mockResponseFileUrlProvider = provider + let getCollections = FeedlyGetCollectionsOperation(service: caller, log: support.log) + + let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: support.log) + mirrorCollectionsAsFolders.addDependency(getCollections) + + let createFolders = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: mirrorCollectionsAsFolders, log: support.log) + createFolders.addDependency(mirrorCollectionsAsFolders) + + let completionExpectation = expectation(description: "Did Finish") + createFolders.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperations([getCollections, mirrorCollectionsAsFolders, createFolders], waitUntilFinished: false) + + waitForExpectations(timeout: 2) + + support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "emptyCollections", subdirectory: subdirectory) + + guard let folder = account.folders?.first else { + XCTFail("Unable to load test folder to add a feed into.") + return nil + } + + XCTAssertEqual(folder.topLevelWebFeeds.count, 0) + + return folder + } + + func expectationForCompletion(of progress: DownloadProgress) -> XCTestExpectation { + return expectation(forNotification: .DownloadProgressDidChange, object: progress) { notification -> Bool in + guard let progress = notification.object as? DownloadProgress else { + return false + } + // We want to assert the progress completes. + if progress.isComplete { + return true + } + return false + } + } + + let searchUrl = "https://macrumors.com" + + func testCancel() { + guard let folder = getFolderByLoadingInitialContent() else { + return + } + + let progress = DownloadProgress(numberOfTasks: 0) + let _ = expectationForCompletion(of: progress) + + let addNewFeed = try! FeedlyAddNewFeedOperation(account: account, + credentials: support.accessToken, + url: searchUrl, + feedName: nil, + searchService: caller, + addToCollectionService: caller, + syncUnreadIdsService: caller, + getStreamContentsService: caller, + container: folder, + progress: progress, + log: support.log) + + // If this expectation is not fulfilled, the operation is not calling `didFinish`. + let completionExpectation = expectation(description: "Did Finish") + addNewFeed.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(addNewFeed) + + XCTAssert(progress.numberRemaining > 0) + + addNewFeed.cancel() + + waitForExpectations(timeout: 2) + + XCTAssert(progress.isComplete) + } + + func testAddNewFeedSuccess() { + guard let folder = getFolderByLoadingInitialContent() else { + return + } + + let progress = DownloadProgress(numberOfTasks: 0) + let _ = expectationForCompletion(of: progress) + + let subdirectory = "feedly-add-new-feed" + let searchUrl = self.searchUrl + let provider = MockResponseProvider(findingMocksIn: subdirectory) + provider.searchQueryHandler = { query in + XCTAssertEqual(query, searchUrl) + } + + transport.mockResponseFileUrlProvider = provider + + let addNewFeed = try! FeedlyAddNewFeedOperation(account: account, + credentials: support.accessToken, + url: searchUrl, + feedName: nil, + searchService: caller, + addToCollectionService: caller, + syncUnreadIdsService: caller, + getStreamContentsService: caller, + container: folder, + progress: progress, + log: support.log) + + // If this expectation is not fulfilled, the operation is not calling `didFinish`. + let completionExpectation = expectation(description: "Did Finish") + addNewFeed.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(addNewFeed) + + XCTAssert(progress.numberRemaining > 0) + + waitForExpectations(timeout: 2) + + XCTAssert(progress.isComplete) + + support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "feedStream", subdirectory: subdirectory) + support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory) + } + + class TestFeedlyAddFeedToCollectionService: FeedlyAddFeedToCollectionService { + var mockResult: Result<[FeedlyFeed], Error>? + var addFeedExpectation: XCTestExpectation? + var parameterTester: ((FeedlyFeedResourceId, String?, String) -> ())? + + func addFeed(with feedId: FeedlyFeedResourceId, title: String?, toCollectionWith collectionId: String, completionHandler: @escaping (Result<[FeedlyFeed], Error>) -> ()) { + guard let result = mockResult else { + XCTFail("Missing mock result. Test may time out because the completion will not be called.") + return + } + parameterTester?(feedId, title, collectionId) + DispatchQueue.main.async { + completionHandler(result) + self.addFeedExpectation?.fulfill() + } + } + } + + func testAddNewFeedFailure() { + guard let folder = getFolderByLoadingInitialContent() else { + return + } + + let progress = DownloadProgress(numberOfTasks: 0) + let _ = expectationForCompletion(of: progress) + + let subdirectory = "feedly-add-new-feed" + let searchUrl = self.searchUrl + let feedName = "MacRumours with a \"u\" because I am Australian" + let provider = MockResponseProvider(findingMocksIn: subdirectory) + provider.searchQueryHandler = { query in + XCTAssertEqual(query, searchUrl) + } + + transport.mockResponseFileUrlProvider = provider + + let service = TestFeedlyAddFeedToCollectionService() + service.mockResult = .failure(URLError(.timedOut)) + service.addFeedExpectation = expectation(description: "Add New Feed Called") + service.parameterTester = { feedResource, title, collectionId in + XCTAssertEqual(feedResource.id, "feed/http://feeds.macrumors.com/MacRumors-All") + XCTAssertEqual(title, feedName) + XCTAssertEqual(collectionId, folder.externalID) + } + + let addNewFeed = try! FeedlyAddNewFeedOperation(account: account, + credentials: support.accessToken, + url: searchUrl, + feedName: feedName, + searchService: caller, + addToCollectionService: service, + syncUnreadIdsService: caller, + getStreamContentsService: caller, + container: folder, + progress: progress, + log: support.log) + + // If this expectation is not fulfilled, the operation is not calling `didFinish`. + let completionExpectation = expectation(description: "Did Finish") + addNewFeed.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(addNewFeed) + + XCTAssert(progress.numberRemaining > 0) + + waitForExpectations(timeout: 2) + + XCTAssert(progress.isComplete) + + XCTAssertEqual(folder.topLevelWebFeeds.count, 0) + } +} + +private class InitialMockResponseProvider: TestTransportMockResponseProviding { + + let subdirectory: String + + init(findingMocksIn subdirectory: String) { + self.subdirectory = subdirectory + } + + func mockResponseFileUrl(for components: URLComponents) -> URL? { + let bundle = Bundle(for: type(of: self)) + + // When we get a request for the initial collections content, use these results. + if components.path.contains("/v3/collections") { + return bundle.url(forResource: "emptyCollections", withExtension: "json", subdirectory: subdirectory) + } + + return nil + } +} + + +private class MockResponseProvider: TestTransportMockResponseProviding { + + let subdirectory: String + + init(findingMocksIn subdirectory: String) { + self.subdirectory = subdirectory + } + + var searchQueryHandler: ((String) -> ())? + + func mockResponseFileUrl(for components: URLComponents) -> URL? { + let bundle = Bundle(for: type(of: self)) + + let queryItems = components.queryItems ?? [] + let query = queryItems.first(where: { $0.name.contains("query") })?.value + + // When we get the search request, use these results. + if components.path.contains("search/feeds") { + if let query = query { + searchQueryHandler?(query) + } else { + XCTFail("`query` missing from URL query items in search request: \(components)") + } + return bundle.url(forResource: "searchResults", withExtension: "json", subdirectory: subdirectory) + } + + // When we get a request to add a feed, use these results. + if components.path.contains("/v3/collections") && components.path.contains("/feeds") { + return bundle.url(forResource: "putFeed", withExtension: "json", subdirectory: subdirectory) + } + + // When we get a request for the initial collections content, use these results. + if components.path.contains("/v3/collections") { + return bundle.url(forResource: "collections", withExtension: "json", subdirectory: subdirectory) + } + + let continuation = queryItems.first(where: { $0.name.contains("continuation") })?.value + + // When we get a request for unread article ids, use these results. + if components.path.contains("streams/ids") { + + // if there is a continuation, return the page for it + if let continuation = continuation, let data = continuation.data(using: .utf8) { + let base64 = data.base64EncodedString() // at least base64 can be used as a path component. + return bundle.url(forResource: "unreadIds@\(base64)", withExtension: "json", subdirectory: subdirectory) + + } else { + // return first page + return bundle.url(forResource: "unreadIds", withExtension: "json", subdirectory: subdirectory) + } + } + + // When we get a request for the contents of the feed stream, use these results. + if components.path.contains("streams/contents") { + + // if there is a continuation, return the page for it + if let continuation = continuation, let data = continuation.data(using: .utf8) { + let base64 = data.base64EncodedString() // at least base64 can be used as a path component. + return bundle.url(forResource: "feedStream@\(base64)", withExtension: "json", subdirectory: subdirectory) + + } else { + // return first page + return bundle.url(forResource: "feedStream", withExtension: "json", subdirectory: subdirectory) + } + } + + return nil + } +} diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/collections.json b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/collections.json new file mode 100644 index 000000000..85503b452 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/collections.json @@ -0,0 +1 @@ +[{"customizable":true,"feeds":[{"feedId":"feed/http://feeds.macrumors.com/MacRumors-All","id":"feed/http://feeds.macrumors.com/MacRumors-All","title":"MacRumors: Mac News and Rumors - All Stories","updated":1575325551366,"velocity":54.4,"subscribers":3,"website":"https://www.macrumors.com","language":"en","description":"Apple, iPhone, iPad, Mac News and Rumors"}],"label":"Mac","created":1575325095480,"enterprise":false,"numFeeds":1,"id":"user/c665fbfb-36e1-485a-918b-90d91f2ebd01/category/da60a29f-94ba-4856-b64f-2830d9859ee8"}] \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/emptyCollections.json b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/emptyCollections.json new file mode 100644 index 000000000..8c1d8737d --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/emptyCollections.json @@ -0,0 +1 @@ +[{"customizable":true,"feeds":[],"label":"Mac","created":1575325095480,"enterprise":false,"numFeeds":0,"id":"user/c665fbfb-36e1-485a-918b-90d91f2ebd01/category/da60a29f-94ba-4856-b64f-2830d9859ee8"}] \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/feedStream.json b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/feedStream.json new file mode 100644 index 000000000..44c38abc9 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/feedStream.json @@ -0,0 +1 @@ +{"id":"user/c665fbfb-36e1-485a-918b-90d91f2ebd01/category/da60a29f-94ba-4856-b64f-2830d9859ee8","title":"MacRumors: Mac News and Rumors - All Stories","updated":1575325551366,"alternate":[{"href":"https://www.macrumors.com","type":"text/html"}],"direction":"ltr","items":[{"id":"i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec8b84b06:105f8:40944486","keywords":["lawsuit","Butterfly Keyboard Issues"],"originId":"https://www.macrumors.com/2019/12/02/apple-butterfly-keyboard-lawsuit-proceeds/","fingerprint":"a52a9c65","title":"Lawsuit Against Apple's Faulty Butterfly Keyboards Moves Forward","author":"Juli Clover","summary":{"content":"A federal judge this week rejected Apple's request to dismiss a class action lawsuit over its faulty butterfly keyboards, reports Reuters, which means the lawsuit will proceed.\n
\n
\nThe complainants believe that Apple knew of and concealed the fact that its 2015 and later MacBook models had keyboards prone to failure and that its repair program does not serve as an effective fix because replacement butterfly keyboards can also fail.\n
\n
\n\"\"\n
\nSan Jose District Judge Edward Davila said that Apple must face the claims that the repair program is inadequate or compensate customers for their out-of-pocket expenses for repairs.\n
\n
\nThe lawsuit covers customers who purchased 2015 MacBook models or later and 2016 MacBook Pro models or later, which includes all machines that are equipped with Apple's butterfly keyboard. Apple has faced public scrutiny and many, many complaints over the butterfly keyboard's penchant to fail when exposed to dust and other small particulates.\n
\n
\nApple launched a repair program that covers all of its MacBook, ‌MacBook Pro‌, and MacBook Air models that have a butterfly keyboard, but at the current time, all keyboard replacements are also butterfly keyboards.\n
\n
\nApple has attempted to revise the butterfly keyboard several times to make it more durable, but ultimately, it's still prone to failure.\n
\n
\n\"\"\n
\nWith the recently released 16-inch MacBook Pro Apple eliminated the butterfly mechanism and reverted to a more reliable scissor mechanism, but the new 16-inch ‌MacBook Pro‌ keyboard is limited to that machine and older models are still getting repairs with butterfly keyboards.\n
\n
\nThe lawsuit is seeking damages for violations of consumer protection laws in several dates. Benjamin Johns, lawyer for the plaintiffs, told Reuters that he's pleased with the decision and looks forward to pursuing the case.


This article, "Lawsuit Against Apple's Faulty Butterfly Keyboards Moves Forward" first appeared on MacRumors.com

Discuss this article in our forums

\n \n
\"\"","direction":"ltr"},"alternate":[{"href":"https://www.macrumors.com/2019/12/02/apple-butterfly-keyboard-lawsuit-proceeds/","type":"text/html"}],"crawled":1575325551366,"published":1575323381000,"origin":{"streamId":"feed/http://feeds.macrumors.com/MacRumors-All","title":"MacRumors: Mac News and Rumors - All Stories","htmlUrl":"https://www.macrumors.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/c665fbfb-36e1-485a-918b-90d91f2ebd01/category/da60a29f-94ba-4856-b64f-2830d9859ee8","label":"Mac"}]},{"id":"i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec84a47ff:10035:40944486","keywords":["Apple ads","Shot on iPhone"],"originId":"https://www.macrumors.com/2019/12/02/apple-shot-on-iphone-11-snowbrawl-video/","fingerprint":"bc3ba7a4","title":"Apple Shares New 'Snowbrawl' Shot on iPhone 11 Pro Video","author":"Juli Clover","summary":{"content":"Apple this afternoon shared a new video in its long running "Shot on iPhone" series, this time showing off the photographic capabilities of the iPhone 11 Pro.\n
\n
\nThe new "Snowbrawl" video features a cinematic snowball fight between two warring teams.\n
\n
\n
\n
\n"Make your holiday videos epic with the highest-quality video in a smartphone, ever," reads the tagline for the video, which was directed by David Leitch.\n
\n
\nApple has also shared a companion video that offers up a look behind the scenes at how the new ad was shot.\n
\n
\n
\n
\nApple has shared many "Shot on ‌iPhone‌" photos and videos over the years, updating the available content with the launch of each new ‌iPhone‌ model.\n
\n
\nApple's ‌iPhone 11 Pro‌, which is used for the newest ad, features a triple-lens camera with the best wide-angle sensor Apple has released so far along with a telephoto lens and a super wide-angle lens for better landscape shots.


This article, "Apple Shares New 'Snowbrawl' Shot on iPhone 11 Pro Video" first appeared on MacRumors.com

Discuss this article in our forums

\n \n
\"\"","direction":"ltr"},"alternate":[{"href":"https://www.macrumors.com/2019/12/02/apple-shot-on-iphone-11-snowbrawl-video/","type":"text/html"}],"crawled":1575318341631,"published":1575317751000,"origin":{"streamId":"feed/http://feeds.macrumors.com/MacRumors-All","title":"MacRumors: Mac News and Rumors - All Stories","htmlUrl":"https://www.macrumors.com"},"visual":{"url":"none"},"unread":true,"categories":[{"id":"user/c665fbfb-36e1-485a-918b-90d91f2ebd01/category/da60a29f-94ba-4856-b64f-2830d9859ee8","label":"Mac"}]},{"id":"i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec81358b5:fd6a:40944486","keywords":["iPhone 12"],"originId":"https://www.macrumors.com/2019/12/02/2020-four-iphone-lineup-analyst-prediction/","fingerprint":"76f872a0","title":"Analyst: Apple to Release Four iPhones in Fall 2020 With 5G Connectivity and OLED Displays","author":"Juli Clover","summary":{"content":"Apple will release four new iPhone models during its fall update cycle, all of which will support 5G connectivity, according to JPMorgan analyst Samik Chatterjee (via CNBC).\n
\n
\nBased on channel checks, Chatterjee believes Apple will release a 5.4-inch ‌iPhone‌, two 6.1-inch iPhones, and one 6.7-inch ‌iPhone‌.\n
\n
\n\"\"\n
\nChatterjee's prediction is a bit of a deviation from current rumors, which have suggested we'll see a lineup similar to the 2019 lineup with 5.4 and 6.7-inch higher-end iPhones and a lower-cost 6.1-inch device.\n
\n
\nApple could, however, be planning to release two higher-end devices in 6.1 and 6.7-inch screen sizes, and two lower-end devices in 5.4 and 6.1-inch screen sizes as Chatterjee suggests, though it's not clear yet if this is the company's plan. Regardless of how many iPhones are released, the entire ‌iPhone‌ lineup is expected to use OLED displays and 5G technology.
"The 2H20 lineup will include all OLED phones, with screen sizes of 5.4″ (one model), 6.1″ (two), and 6.7″ (one), broadening the screen size range from 5.8″ to 6.5″ in 2019. We expect the two higher end models (one 6.1″, one 6.7″) to include mmWave support, triple camera and World facing 3D sensing, while the lower-end models (one 6.1″, one 5.4″) will include support for only sub-6 GHz and dual camera (no World-facing 3D sensing)."
Two of the higher-end iPhones will be equipped with new rear camera technology with "world-facing" 3D sensing for improved augmented reality capabilities, while the others will use dual-lens setups similar to the iPhone 11.\n
\n
\nThere could also be a split between the type of 5G connectivity offered by each ‌iPhone‌. The two higher-end iPhones could offer support for the fastest 5G technology, mmWave, while the two lower-end iPhones may be limited to the sub-6GHz spectrum, which is not as speedy but has a wider range.\n
\n
\nmmWave 5G technology will likely be limited to major cities and dense urban areas because of its short range, while 5G networks in rural and suburban areas will use the slower sub-6GHz technology, such as the 600MHz network T-Mobile is rolling out.\n
\n
\nRumors have suggested Apple is going to use Qualcomm's X55 modems in all of its 2020 iPhones, and while those modems do support both mmWave and sub-6GHz spectrum, an additional mmWave antenna is needed for mmWave support.\n
\n
\nTo make the lower-end iPhones more affordable, Apple could potentially limit the mmWave antenna to higher-end models, though prior rumors have suggested Apple's aim with its 2020 iPhones is to match the technology in more affordable 5G Android smartphones, which do support mmWave.\n
\n
\nStarting in 2021, Chatterjee believes Apple will make some significant changes to its ‌iPhone‌ release cycles. "Based on our supply chain checks, we are expecting a strategic change in the launch cadence with the release of two new ‌iPhone‌ models in 1H21 followed by another two in 2H21, which will serve to smooth seasonality around the launch," he wrote.\n
\n
\nReleasing two iPhones during the first half of 2021 and two iPhones during the second half of 2021 could allow Apple to better compete with rival smartphone companies that introduce new devices throughout the year and limit "product cycle missteps" by allowing for designs to be altered more quickly in response to market feedback.\n
\n
\nApple has been launching new iPhones in the fall since 2011, and the note from Chatterjee provides little additional information on the potential split if Apple does pursue a new launch timeline.\n
\n
\nApple could release lower-end devices earlier in the year and then save its higher-end launches for the fall, but Chatterjee's current prediction would see Apple releasing four iPhones in September 2020 and then two additional iPhones in the first half of 2021, for a total of six iPhones within six months, which seems like a bit of a stretch.

Related Roundup: iPhone 12

This article, "Analyst: Apple to Release Four iPhones in Fall 2020 With 5G Connectivity and OLED Displays" first appeared on MacRumors.com

Discuss this article in our forums

\n \n
\"\"","direction":"ltr"},"alternate":[{"href":"https://www.macrumors.com/2019/12/02/2020-four-iphone-lineup-analyst-prediction/","type":"text/html"}],"crawled":1575314741429,"published":1575312344000,"origin":{"streamId":"feed/http://feeds.macrumors.com/MacRumors-All","title":"MacRumors: Mac News and Rumors - All Stories","htmlUrl":"https://www.macrumors.com"},"unread":true,"categories":[{"id":"user/c665fbfb-36e1-485a-918b-90d91f2ebd01/category/da60a29f-94ba-4856-b64f-2830d9859ee8","label":"Mac"}]}]} \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/putFeed.json b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/putFeed.json new file mode 100644 index 000000000..237de7f21 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/putFeed.json @@ -0,0 +1 @@ +[{"feedId":"feed/http://feeds.macrumors.com/MacRumors-All","id":"feed/http://feeds.macrumors.com/MacRumors-All","title":"MacRumors: Mac News and Rumors - All Stories","updated":1575318341631,"velocity":54.4,"subscribers":2,"website":"https://www.macrumors.com","language":"en","description":"Apple, iPhone, iPad, Mac News and Rumors"}] \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/searchResults.json b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/searchResults.json new file mode 100644 index 000000000..0819a3cfe --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/searchResults.json @@ -0,0 +1 @@ +{"results":[{"feedId":"feed/http://feeds.macrumors.com/MacRumors-All","score":1.1779602,"lastUpdated":1535409540000,"coverage":0.0,"averageReadTime":0.0,"coverageScore":0.0,"tagCounts":{"ios":1},"totalTagCount":1,"scheme":"u:b:o","id":"feed/http://feeds.macrumors.com/MacRumors-All","title":"MacRumors: Mac News and Rumors - All Stories","updated":1535409540000,"velocity":54.4,"subscribers":3,"website":"https://www.macrumors.com","language":"en","description":"Apple, iPhone, iPad, Mac News and Rumors","deliciousTags":["ios"]}],"queryType":"url","related":["ios"],"scheme":"u:b:o"} \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/unreadIds.json b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/unreadIds.json new file mode 100644 index 000000000..6e2945dd8 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/feedly-add-new-feed/unreadIds.json @@ -0,0 +1,15 @@ +{ + "ids": [ + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec8b84b06:105f8:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec84a47ff:10035:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec81358b5:fd6a:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec7dc2863:f9eb:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec7a41b17:f62e:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec76d1e3f:ee16:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec76d1e3f:ee15:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec735cb51:eb40:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec6fecd41:e87d:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec6fecd41:e87c:40944486", + "i5dK0N4LYkCdW27N2hxMjfOjdU7G+cHFwqWTCuim9HU=_16ec6c7d2b2:e5c8:40944486" + ] +} diff --git a/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift b/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift index 8d4bb9f36..b39110a8f 100644 --- a/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift @@ -61,6 +61,13 @@ struct FeedlyCategoryResourceId: FeedlyResourceId { let id = "user/\(userId)/category/global.all" return FeedlyCategoryResourceId(id: id) } + + /// All articles from all the feeds the user loves most. + static func mustRead(for userId: String) -> FeedlyCategoryResourceId { + // https://developer.feedly.com/cloud/#global-resource-ids + let id = "user/\(userId)/category/global.must" + return FeedlyCategoryResourceId(id: id) + } } } From f58bb38e8fce39a16b99d03216c99883565e832a Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Tue, 3 Dec 2019 21:46:27 +1100 Subject: [PATCH 3/3] Remove unused code. --- .../Operations/FeedlyAddNewFeedOperation.swift | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift index db56fc0c4..43acad907 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -10,17 +10,6 @@ import Foundation import os.log import RSWeb -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 @@ -66,7 +55,11 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl override func cancel() { operationQueue.cancelAllOperations() super.cancel() + didFinish() + + // Operation should silently cancel. + addCompletionHandler = nil } override func main() {