mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Search Feedly for the best feed (and its identifier) for the URL entered when adding a new feed. #1300
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
9EA643CE2391D3550018A28C /* FeedlyAddExistingFeedOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAddExistingFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9EA643D2239305680018A28C /* FeedlySearchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySearchOperation.swift; sourceTree = "<group>"; };
|
||||
9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedsSearchResponse.swift; sourceTree = "<group>"; };
|
||||
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = "<group>"; };
|
||||
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = "<group>"; };
|
||||
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntry.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -226,6 +226,55 @@ final class FeedlyAPICaller {
|
||||
}
|
||||
}
|
||||
|
||||
func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
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<Void, Error>) -> ()) {
|
||||
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<FeedlyFeedsSearchResponse, Error>) -> ()) {
|
||||
|
||||
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<Void, Error>) -> ()) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import Foundation
|
||||
|
||||
struct FeedlyFeed: Codable {
|
||||
var feedId: String
|
||||
var id: String
|
||||
var title: String?
|
||||
var updated: Date?
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate,
|
||||
|
||||
var addCompletionHandler: ((Result<Void, Error>) -> ())?
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<WebFeed, Error>) -> ())?
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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<FeedlyFeedsSearchResponse, Error>) -> ())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user