Implements creating, updating, moving and removing feeds.
This commit is contained in:
parent
6443d0da97
commit
41ca023c31
|
@ -115,6 +115,7 @@
|
|||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; };
|
||||
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */; };
|
||||
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */; };
|
||||
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -287,6 +288,7 @@
|
|||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = "<group>"; };
|
||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = "<group>"; };
|
||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = "<group>"; };
|
||||
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegateError.swift; sourceTree = "<group>"; };
|
||||
D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = "<group>"; };
|
||||
D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = "<group>"; };
|
||||
D511EEB7202422BB00712EC3 /* Account_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_release.xcconfig; sourceTree = "<group>"; };
|
||||
|
@ -540,6 +542,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */,
|
||||
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */,
|
||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */,
|
||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
|
||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */,
|
||||
|
@ -783,6 +786,7 @@
|
|||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
|
||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
|
||||
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */,
|
||||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
|
||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
||||
9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */,
|
||||
|
|
|
@ -387,6 +387,98 @@ final class FeedlyAPICaller {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
guard let encodedId = encodeForURLPath(collectionId) else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.percentEncodedPath = "/v3/collections/\(encodedId)/feeds"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
do {
|
||||
struct AddFeedBody: Encodable {
|
||||
var id: String
|
||||
var title: String?
|
||||
}
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(AddFeedBody(id: feedId.id, title: title))
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(_, let collectionFeeds):
|
||||
if let feeds = collectionFeeds {
|
||||
completionHandler(.success(feeds))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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), let encodedFeedId = encodeForURLPath(feedId) else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
|
|
@ -155,7 +155,8 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
folder.externalID = collection.id
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
// Is the name empty? Or one of the global resource names?
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToAddFolder(name)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
|
@ -165,25 +166,34 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let id = folder.externalID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRenameFolder(folder.nameForDisplay, name)))
|
||||
}
|
||||
}
|
||||
|
||||
let nameBefore = folder.name
|
||||
|
||||
caller.renameCollection(with: id, to: name) { result in
|
||||
switch result {
|
||||
case .success(let collection):
|
||||
folder.name = collection.label
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
folder.name = nameBefore
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
folder.name = name
|
||||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let id = folder.externalID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFolder(folder.nameForDisplay)))
|
||||
}
|
||||
}
|
||||
|
||||
caller.deleteCollection(with: id) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
@ -195,24 +205,182 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
private func isValidContainer(for account: Account, container: Container) throws -> (Folder, String) {
|
||||
guard let folder = container as? Folder else {
|
||||
throw FeedlyAccountDelegateError.addFeedChooseFolder
|
||||
}
|
||||
|
||||
guard let collectionId = folder.externalID else {
|
||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||
}
|
||||
|
||||
guard let userId = credentials?.username else {
|
||||
throw FeedlyAccountDelegateError.notLoggedIn
|
||||
}
|
||||
|
||||
let uncategorized = FeedlyCategoryResourceId.uncategorized(for: userId)
|
||||
|
||||
guard collectionId != uncategorized.id else {
|
||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||
}
|
||||
|
||||
return (folder, collectionId)
|
||||
}
|
||||
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
fatalError()
|
||||
let (folder, collectionId): (Folder, String)
|
||||
do {
|
||||
(folder, collectionId) = try isValidContainer(for: account, container: container)
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
let resourceId = FeedlyFeedResourceId(url: url)
|
||||
|
||||
caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { result in
|
||||
switch result {
|
||||
case .success(let feedlyFeeds):
|
||||
let feedsBefore = folder.flattenedFeeds()
|
||||
for feedlyFeed in feedlyFeeds where !account.hasFeed(with: feedlyFeed.feedId) {
|
||||
let resourceId = FeedlyFeedResourceId(id: feedlyFeed.id)
|
||||
let feed = account.createFeed(with: feedlyFeed.title,
|
||||
url: resourceId.url,
|
||||
feedID: feedlyFeed.id,
|
||||
homePageURL: feedlyFeed.website)
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
let feedsAfter = folder.flattenedFeeds()
|
||||
let added = feedsAfter.subtracting(feedsBefore)
|
||||
if let feed = added.first {
|
||||
completion(.success(feed))
|
||||
} else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID }
|
||||
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
let feedId = FeedlyFeedResourceId(id: feed.feedID)
|
||||
let editedNameBefore = feed.editedName
|
||||
|
||||
caller.addFeed(with: feedId, title: name, toCollectionWith: collectionId) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
feed.editedName = editedNameBefore
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
// optimistically set the name
|
||||
feed.editedName = name
|
||||
}
|
||||
|
||||
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
let (folder, collectionId): (Folder, String)
|
||||
do {
|
||||
(folder, collectionId) = try isValidContainer(for: account, container: container)
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
let feedId = FeedlyFeedResourceId(id: with.feedID)
|
||||
|
||||
caller.addFeed(with: feedId, toCollectionWith: collectionId) { result in
|
||||
switch result {
|
||||
case .success(let feedlyFeeds):
|
||||
for feedlyFeed in feedlyFeeds where !folder.hasFeed(with: feedlyFeed.feedId) {
|
||||
let feed: Feed = {
|
||||
if with.url == FeedlyFeedResourceId(id: feedlyFeed.id).url {
|
||||
with.metadata.feedID = feedlyFeed.id
|
||||
with.name = feedlyFeed.title
|
||||
with.homePageURL = feedlyFeed.website
|
||||
return with
|
||||
} else {
|
||||
let resourceId = FeedlyFeedResourceId(id: feedlyFeed.id)
|
||||
return account.createFeed(with: feedlyFeed.title,
|
||||
url: resourceId.url,
|
||||
feedID: feedlyFeed.id,
|
||||
homePageURL: feedlyFeed.website)
|
||||
}
|
||||
}()
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
guard let folder = container as? Folder, let collectionId = folder.externalID else {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
|
||||
}
|
||||
}
|
||||
|
||||
caller.removeFeed(feed.feedID, fromCollectionWith: collectionId) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
folder.addFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
guard let from = from as? Folder, let to = to as? Folder else {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
|
||||
}
|
||||
}
|
||||
|
||||
addFeed(for: account, with: feed, to: to) { [weak self] addResult in
|
||||
switch addResult {
|
||||
// now that we have added the feed, remove it from the other collection
|
||||
case .success:
|
||||
self?.removeFeed(for: account, with: feed, from: from) { removeResult in
|
||||
switch removeResult {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure:
|
||||
from.addFeed(feed)
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
from.addFeed(feed)
|
||||
to.removeFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// optimistically move the feed, undoing as appropriate to the failure
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// FeedlyAccountDelegateError.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 9/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FeedlyAccountDelegateError: LocalizedError {
|
||||
case notLoggedIn
|
||||
case unableToAddFolder(String)
|
||||
case unableToRenameFolder(String, String)
|
||||
case unableToRemoveFolder(String)
|
||||
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
|
||||
case addFeedChooseFolder
|
||||
case addFeedInvalidFolder(Folder)
|
||||
case unableToRemoveFeed(Feed)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notLoggedIn:
|
||||
return NSLocalizedString("Please add the Feedly account again.", comment: "Feedly - Credentials not found.")
|
||||
|
||||
case .unableToAddFolder(let name):
|
||||
let template = NSLocalizedString("Could not create a folder named \"%@\".", comment: "Feedly - Could not create a folder/collection.")
|
||||
return String(format: template, name)
|
||||
|
||||
case .unableToRenameFolder(let from, let to):
|
||||
let template = NSLocalizedString("Could not rename \"%@\" to \"%@\".", comment: "Feedly - Could not rename a folder/collection.")
|
||||
return String(format: template, from, to)
|
||||
|
||||
case .unableToRemoveFolder(let name):
|
||||
let template = NSLocalizedString("Could not remove the folder named \"%@\".", comment: "Feedly - Could not remove a folder/collection.")
|
||||
return String(format: template, name)
|
||||
|
||||
case .unableToMoveFeedBetweenFolders(let feed, _, let to):
|
||||
let template = NSLocalizedString("Could not move \"%@\" to \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.")
|
||||
return String(format: template, feed.nameForDisplay, to.nameForDisplay)
|
||||
|
||||
case .addFeedChooseFolder:
|
||||
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly - Feed can only be added to folders.")
|
||||
|
||||
case .addFeedInvalidFolder(let invalidFolder):
|
||||
let template = NSLocalizedString("Feeds cannot be added to the \"%@\" folder.", comment: "Feedly - Feed can only be added to folders.")
|
||||
return String(format: template, invalidFolder.nameForDisplay)
|
||||
|
||||
case .unableToRemoveFeed(let feed):
|
||||
let template = NSLocalizedString("Could not remove \"%@\".", comment: "Feedly - Could not remove a feed.")
|
||||
return String(format: template, feed.nameForDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .notLoggedIn:
|
||||
return nil
|
||||
|
||||
case .unableToAddFolder:
|
||||
return nil
|
||||
|
||||
case .unableToRenameFolder:
|
||||
return nil
|
||||
|
||||
case .unableToRemoveFolder:
|
||||
return nil
|
||||
|
||||
case .unableToMoveFeedBetweenFolders(let feed, let from, let to):
|
||||
let template = NSLocalizedString("\"%@\" may be in both \"%@\" and \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.")
|
||||
return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)
|
||||
|
||||
case .addFeedChooseFolder:
|
||||
return nil
|
||||
|
||||
case .addFeedInvalidFolder:
|
||||
return NSLocalizedString("Please choose a different folder to contain the feed.", comment: "Feedly - Feed can only be added to folders recovery suggestion.")
|
||||
|
||||
case .unableToRemoveFeed:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,17 +13,15 @@ protocol FeedlyResourceId {
|
|||
|
||||
/// The resource Id from Feedly.
|
||||
var id: String { get }
|
||||
|
||||
/// 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.
|
||||
var url: String { get }
|
||||
}
|
||||
|
||||
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
|
||||
struct FeedlyFeedResourceId: FeedlyResourceId {
|
||||
var id: String
|
||||
|
||||
/// 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.
|
||||
var url: String {
|
||||
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
|
||||
var mutant = id
|
||||
|
@ -35,3 +33,19 @@ struct FeedlyFeedResourceId: FeedlyResourceId {
|
|||
return id
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyFeedResourceId {
|
||||
init(url: String) {
|
||||
self.id = "feed/\(url)"
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedlyCategoryResourceId: FeedlyResourceId {
|
||||
var id: String
|
||||
|
||||
static func uncategorized(for userId: String) -> FeedlyCategoryResourceId {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/category/global.uncategorized"
|
||||
return FeedlyCategoryResourceId(id: id)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue