Create Feedly module.

This commit is contained in:
Brent Simmons
2024-04-07 20:52:34 -07:00
parent 6db1d40597
commit 4b0e7addc9
65 changed files with 401 additions and 287 deletions

View File

@@ -26,6 +26,7 @@ let package = Package(
.package(path: "../Feedbin"),
.package(path: "../LocalAccount"),
.package(path: "../FeedFinder"),
.package(path: "../Feedly"),
.package(path: "../CommonErrors")
],
targets: [
@@ -47,7 +48,8 @@ let package = Package(
"Feedbin",
"LocalAccount",
"FeedFinder",
"CommonErrors"
"CommonErrors",
"Feedly"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")

View File

@@ -14,6 +14,7 @@ import os.log
import Secrets
import Core
import CommonErrors
import Feedly
final class FeedlyAccountDelegate: AccountDelegate {
@@ -761,7 +762,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
@MainActor func accountWillBeDeleted(_ account: Account) {
let logout = FeedlyLogoutOperation(account: account, service: caller, log: log)
let logout = FeedlyLogoutOperation(service: caller, log: log)
// Dispatch on the shared queue because the lifetime of the account delegate is uncertain.
MainThreadOperationQueue.shared.add(logout)
}

View File

@@ -9,6 +9,7 @@
import Foundation
import Web
import Secrets
import Feedly
protocol FeedlyAPICallerDelegate: AnyObject {
/// Implemented by the `FeedlyAccountDelegate` reauthorize the client with a fresh OAuth token so the client can retry the unauthorized request.
@@ -390,7 +391,7 @@ final class FeedlyAPICaller {
extension FeedlyAPICaller: FeedlyAddFeedToCollectionService {
func addFeed(with feedId: FeedlyFeedResourceID, title: String? = nil, toCollectionWith collectionID: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) {
@MainActor func addFeed(with feedId: FeedlyFeedResourceID, title: String? = nil, toCollectionWith collectionID: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) {
guard !isSuspended else {
return DispatchQueue.main.async {
completion(.failure(TransportError.suspended))

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Feedly
@MainActor struct FeedlyFeedContainerValidator {
var container: Container

View File

@@ -1,29 +0,0 @@
//
// FeedlyEntryIdentifierProviding.swift
// Account
//
// Created by Kiel Gillard on 9/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
protocol FeedlyEntryIdentifierProviding: AnyObject {
@MainActor var entryIDs: Set<String> { get }
}
final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding {
private (set) var entryIDs: Set<String>
init(entryIDs: Set<String> = Set()) {
self.entryIDs = entryIDs
}
@MainActor func addEntryIDs(from provider: FeedlyEntryIdentifierProviding) {
entryIDs.formUnion(provider.entryIDs)
}
@MainActor func addEntryIDs(in articleIDs: [String]) {
entryIDs.formUnion(articleIDs)
}
}

View File

@@ -10,6 +10,7 @@ import Foundation
import AuthenticationServices
import Secrets
import Core
import Feedly
public protocol OAuthAccountAuthorizationOperationDelegate: AnyObject {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account)

View File

@@ -8,6 +8,7 @@
import Foundation
import Web
import Feedly
/// Models section 6 of the OAuth 2.0 Authorization Framework
/// https://tools.ietf.org/html/rfc6749#section-6

View File

@@ -8,6 +8,7 @@
import Foundation
import Secrets
import Feedly
extension OAuthAuthorizationClient {

View File

@@ -9,6 +9,7 @@
import Foundation
import Web
import Secrets
import Feedly
/// Client-specific information for requesting an authorization code grant.
/// Accounts are responsible for the scope.

View File

@@ -11,6 +11,7 @@ import os.log
import Web
import Secrets
import Core
import Feedly
@MainActor final class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate {

View File

@@ -8,6 +8,7 @@
import Foundation
import CommonErrors
import Feedly
protocol FeedlyAddFeedToCollectionService {
func addFeed(with feedId: FeedlyFeedResourceID, title: String?, toCollectionWith collectionID: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ())

View File

@@ -13,8 +13,9 @@ import Web
import Secrets
import Core
import CommonErrors
import Feedly
class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {
final class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {
private let operationQueue = MainThreadOperationQueue()
private let folder: Folder

View File

@@ -7,6 +7,7 @@
//
import Foundation
import Feedly
protocol FeedlyCheckpointOperationDelegate: AnyObject {
@MainActor func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation)

View File

@@ -9,6 +9,7 @@
import Foundation
import os.log
import Core
import Feedly
/// Single responsibility is to accurately reflect Collections and their Feeds as Folders and their Feeds.
final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
@@ -23,7 +24,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
self.log = log
}
override func run() {
public override func run() {
defer {
didFinish()
}

View File

@@ -10,6 +10,7 @@ import Foundation
import os.log
import Web
import Core
import Feedly
class FeedlyDownloadArticlesOperation: FeedlyOperation {
@@ -45,12 +46,11 @@ class FeedlyDownloadArticlesOperation: FeedlyOperation {
Task { @MainActor in
let provider = FeedlyEntryIdentifierProvider(entryIDs: Set(articleIDs))
let getEntries = FeedlyGetEntriesOperation(service: getEntriesService, provider: provider, log: log)
let getEntries = FeedlyGetEntriesOperation(service: self.getEntriesService, provider: provider, log: self.log)
getEntries.delegate = self
self.operationQueue.add(getEntries)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
parsedItemProvider: getEntries,
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(parsedItemProvider: getEntries,
log: log)
organiseByFeed.delegate = self
organiseByFeed.addDependency(getEntries)

View File

@@ -8,6 +8,7 @@
import Foundation
import os.log
import Feedly
final class FeedlyFetchIDsForMissingArticlesOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {

View File

@@ -10,6 +10,7 @@ import Foundation
import os.log
import SyncDatabase
import Secrets
import Feedly
/// Clone locally the remote starred article state.
///

View File

@@ -10,6 +10,7 @@ import Foundation
import os.log
import Secrets
import Database
import Feedly
/// Ensure a status exists for every article id the user might be interested in.
///

View File

@@ -11,6 +11,7 @@ import os.log
import Parser
import SyncDatabase
import Secrets
import Feedly
/// Clone locally the remote unread article state.
///
@@ -27,12 +28,12 @@ final class FeedlyIngestUnreadArticleIDsOperation: FeedlyOperation {
private var remoteEntryIDs = Set<String>()
private let log: OSLog
convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
public convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
let resource = FeedlyCategoryResourceID.Global.all(for: userID)
self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log)
}
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
public init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
self.account = account
self.resource = resource
self.service = service

View File

@@ -8,6 +8,7 @@
import Foundation
import os.log
import Feedly
protocol FeedlyFeedsAndFoldersProviding {
@MainActor var feedsAndFolders: [([FeedlyFeed], Folder)] { get }

View File

@@ -10,6 +10,7 @@ import Foundation
import os.log
import Web
import Secrets
import Feedly
final class FeedlyRefreshAccessTokenOperation: FeedlyOperation {

View File

@@ -12,6 +12,7 @@ import SyncDatabase
import Web
import Secrets
import Core
import Feedly
/// Compose the operations necessary to get the entire set of articles, feeds and folders with the statuses the user expects between now and a certain date in the past.
final class FeedlySyncAllOperation: FeedlyOperation {
@@ -83,7 +84,7 @@ final class FeedlySyncAllOperation: FeedlyOperation {
// Get each page of the article ids which have been update since the last successful fetch start date.
// If the date is nil, this operation provides an empty set (everything is new, nothing is updated).
let getUpdated = FeedlyGetUpdatedArticleIDsOperation(account: account, userID: feedlyUserID, service: getStreamIDsService, newerThan: lastSuccessfulFetchStartDate, log: log)
let getUpdated = FeedlyGetUpdatedArticleIDsOperation(userID: feedlyUserID, service: getStreamIDsService, newerThan: lastSuccessfulFetchStartDate, log: log)
getUpdated.delegate = self
getUpdated.downloadProgress = downloadProgress
getUpdated.addDependency(createFeedsOperation)

View File

@@ -12,6 +12,7 @@ import Parser
import Web
import Secrets
import Core
import Feedly
final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
@@ -63,15 +64,14 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD
}
func pageOperations(for continuation: String?) -> [MainThreadOperation] {
let getPage = FeedlyGetStreamContentsOperation(account: account,
resource: resource,
let getPage = FeedlyGetStreamContentsOperation(resource: resource,
service: service,
continuation: continuation,
newerThan: newerThan,
log: log)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: getPage, log: log)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(parsedItemProvider: getPage, log: log)
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: organiseByFeed, log: log)

View File

@@ -10,6 +10,7 @@ import Foundation
import Parser
import os.log
import Database
import Feedly
/// Combine the articles with their feeds for a specific account.
final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {

View File

@@ -11,14 +11,14 @@ import CloudKit
public extension CKRecord {
public var externalID: String {
var externalID: String {
return recordID.externalID
}
}
public extension CKRecord.ID {
public var externalID: String {
var externalID: String {
return recordName
}
}

8
Feedly/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

40
Feedly/Package.swift Normal file
View File

@@ -0,0 +1,40 @@
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "Feedly",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "Feedly",
targets: ["Feedly"]),
],
dependencies: [
.package(path: "../Parser"),
.package(path: "../Articles"),
.package(path: "../Secrets"),
.package(path: "../Core"),
.package(path: "../SyncDatabase"),
.package(path: "../Web"),
],
targets: [
.target(
name: "Feedly",
dependencies: [
"Parser",
"Articles",
"Secrets",
"Core",
"SyncDatabase",
"Web"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "FeedlyTests",
dependencies: ["Feedly"]),
]
)

View File

@@ -8,7 +8,8 @@
import Foundation
enum FeedlyAccountDelegateError: LocalizedError {
public enum FeedlyAccountDelegateError: LocalizedError, Sendable {
case notLoggedIn
case unexpectedResourceID(String)
case unableToAddFolder(String)
@@ -20,7 +21,7 @@ enum FeedlyAccountDelegateError: LocalizedError {
case unableToRenameFeed(String, String)
case unableToRemoveFeed(String)
var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .notLoggedIn:
return NSLocalizedString("Please add the Feedly account again. If this problem persists, open Keychain Access and delete all feedly.com entries, then try again.", comment: "Feedly Credentials not found.")
@@ -62,7 +63,7 @@ enum FeedlyAccountDelegateError: LocalizedError {
}
}
var recoverySuggestion: String? {
public var recoverySuggestion: String? {
switch self {
case .notLoggedIn:
return nil

View File

@@ -8,13 +8,13 @@
import Foundation
protocol FeedlyResourceProviding {
public protocol FeedlyResourceProviding {
@MainActor var resource: FeedlyResourceID { get }
}
extension FeedlyFeedResourceID: FeedlyResourceProviding {
var resource: FeedlyResourceID {
public var resource: FeedlyResourceID {
return self
}
}

View File

@@ -8,7 +8,8 @@
import Foundation
struct FeedlyCategory: Decodable {
let label: String
let id: String
public struct FeedlyCategory: Decodable, Sendable {
public let label: String
public let id: String
}

View File

@@ -8,8 +8,9 @@
import Foundation
struct FeedlyCollection: Codable {
let feeds: [FeedlyFeed]
let label: String
let id: String
public struct FeedlyCollection: Codable, Sendable {
public let feeds: [FeedlyFeed]
public let label: String
public let id: String
}

View File

@@ -8,16 +8,21 @@
import Foundation
struct FeedlyCollectionParser {
let collection: FeedlyCollection
public struct FeedlyCollectionParser: Sendable {
public let collection: FeedlyCollection
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
var folderName: String {
public var folderName: String {
return rightToLeftTextSantizer.sanitize(collection.label) ?? ""
}
var externalID: String {
public var externalID: String {
return collection.id
}
public init(collection: FeedlyCollection) {
self.collection = collection
}
}

View File

@@ -8,58 +8,59 @@
import Foundation
struct FeedlyEntry: Decodable {
public struct FeedlyEntry: Decodable, Sendable {
/// the unique, immutable ID for this particular article.
let id: String
public let id: String
/// the articles title. This string does not contain any HTML markup.
let title: String?
struct Content: Decodable {
enum Direction: String, Decodable {
public let title: String?
public struct Content: Decodable, Sendable {
public enum Direction: String, Decodable, Sendable {
case leftToRight = "ltr"
case rightToLeft = "rtl"
}
let content: String?
let direction: Direction?
public let content: String?
public let direction: Direction?
}
/// This object typically has two values: content for the content itself, and direction (ltr for left-to-right, rtl for right-to-left). The content itself contains sanitized HTML markup.
let content: Content?
public let content: Content?
/// content object the article summary. See the content object above.
let summary: Content?
public let summary: Content?
/// the authors name
let author: String?
public let author: String?
/// the immutable timestamp, in ms, when this article was processed by the feedly Cloud servers.
let crawled: Date
public let crawled: Date
/// the timestamp, in ms, when this article was re-processed and updated by the feedly Cloud servers.
let recrawled: Date?
public let recrawled: Date?
/// the feed from which this article was crawled. If present, streamId will contain the feed id, title will contain the feed title, and htmlUrl will contain the feeds website.
let origin: FeedlyOrigin?
public let origin: FeedlyOrigin?
/// Used to help find the URL to visit an article on a web site.
/// See https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
let canonical: [FeedlyLink]?
public let canonical: [FeedlyLink]?
/// a list of alternate links for this article. Each link object contains a media type and a URL. Typically, a single object is present, with a link to the original web page.
let alternate: [FeedlyLink]?
public let alternate: [FeedlyLink]?
/// Was this entry read by the user? If an Authorization header is not provided, this will always return false. If an Authorization header is provided, it will reflect if the user has read this entry or not.
let unread: Bool
public let unread: Bool
/// a list of tag objects (id and label) that the user added to this entry. This value is only returned if an Authorization header is provided, and at least one tag has been added. If the entry has been explicitly marked as read (not the feed itself), the global.read tag will be present.
let tags: [FeedlyTag]?
public let tags: [FeedlyTag]?
/// a list of category objects (id and label) that the user associated with the feed of this entry. This value is only returned if an Authorization header is provided.
let categories: [FeedlyCategory]?
public let categories: [FeedlyCategory]?
/// A list of media links (videos, images, sound etc) provided by the feed. Some entries do not have a summary or content, only a collection of media links.
let enclosure: [FeedlyLink]?
public let enclosure: [FeedlyLink]?
}

View File

@@ -0,0 +1,30 @@
//
// FeedlyEntryIdentifierProviding.swift
// Account
//
// Created by Kiel Gillard on 9/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public protocol FeedlyEntryIdentifierProviding: AnyObject {
@MainActor var entryIDs: Set<String> { get }
}
public final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding {
private (set) public var entryIDs: Set<String>
public init(entryIDs: Set<String> = Set()) {
self.entryIDs = entryIDs
}
@MainActor public func addEntryIDs(from provider: FeedlyEntryIdentifierProviding) {
entryIDs.formUnion(provider.entryIDs)
}
@MainActor public func addEntryIDs(in articleIDs: [String]) {
entryIDs.formUnion(articleIDs)
}
}

View File

@@ -10,17 +10,18 @@ import Foundation
import Articles
import Parser
struct FeedlyEntryParser {
let entry: FeedlyEntry
public struct FeedlyEntryParser: Sendable {
public let entry: FeedlyEntry
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
var id: String {
public var id: String {
return entry.id
}
/// When ingesting articles, the feedURL must match a feed's `feedID` for the article to be reachable between it and its matching feed. It reminds me of a foreign key.
var feedUrl: String? {
public var feedUrl: String? {
guard let id = entry.origin?.streamID else {
// At this point, check Feedly's API isn't glitching or the response has not changed structure.
assertionFailure("Entries need to be traceable to a feed or this entry will be dropped.")
@@ -31,7 +32,7 @@ struct FeedlyEntryParser {
/// Convoluted external URL logic "documented" here:
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
var externalUrl: String? {
public var externalUrl: String? {
let multidimensionalArrayOfLinks = [entry.canonical, entry.alternate]
let withExistingValues = multidimensionalArrayOfLinks.compactMap { $0 }
let flattened = withExistingValues.flatMap { $0 }
@@ -39,32 +40,32 @@ struct FeedlyEntryParser {
return webPageLinks.first?.href
}
var title: String? {
public var title: String? {
return rightToLeftTextSantizer.sanitize(entry.title)
}
var contentHMTL: String? {
public var contentHMTL: String? {
return entry.content?.content ?? entry.summary?.content
}
var contentText: String? {
public var contentText: String? {
// We could strip HTML from contentHTML?
return nil
}
var summary: String? {
public var summary: String? {
return rightToLeftTextSantizer.sanitize(entry.summary?.content)
}
var datePublished: Date {
public var datePublished: Date {
return entry.crawled
}
var dateModified: Date? {
public var dateModified: Date? {
return entry.recrawled
}
var authors: Set<ParsedAuthor>? {
public var authors: Set<ParsedAuthor>? {
guard let name = entry.author else {
return nil
}
@@ -72,14 +73,14 @@ struct FeedlyEntryParser {
}
/// While there is not yet a tagging interface, articles can still be searched for by tags.
var tags: Set<String>? {
public var tags: Set<String>? {
guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else {
return nil
}
return Set(labels)
}
var attachments: Set<ParsedAttachment>? {
public var attachments: Set<ParsedAttachment>? {
guard let enclosure = entry.enclosure, !enclosure.isEmpty else {
return nil
}
@@ -87,7 +88,7 @@ struct FeedlyEntryParser {
return attachments.isEmpty ? nil : Set(attachments)
}
var parsedItemRepresentation: ParsedItem? {
public var parsedItemRepresentation: ParsedItem? {
guard let feedUrl = feedUrl else {
return nil
}

View File

@@ -8,9 +8,10 @@
import Foundation
struct FeedlyFeed: Codable {
let id: String
let title: String?
let updated: Date?
let website: String?
public struct FeedlyFeed: Codable, Sendable {
public let id: String
public let title: String?
public let updated: Date?
public let website: String?
}

View File

@@ -8,25 +8,31 @@
import Foundation
struct FeedlyFeedParser {
let feed: FeedlyFeed
public struct FeedlyFeedParser: Sendable {
public let feed: FeedlyFeed
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
var title: String? {
public var title: String? {
return rightToLeftTextSantizer.sanitize(feed.title) ?? ""
}
var feedID: String {
public var feedID: String {
return feed.id
}
var url: String {
public var url: String {
let resource = FeedlyFeedResourceID(id: feed.id)
return resource.url
}
var homePageURL: String? {
public var homePageURL: String? {
return feed.website
}
public init(feed: FeedlyFeed) {
self.feed = feed
}
}

View File

@@ -8,12 +8,13 @@
import Foundation
struct FeedlyFeedsSearchResponse: Decodable {
struct Feed: Decodable {
let title: String
let feedId: String
public struct FeedlyFeedsSearchResponse: Decodable, Sendable {
public struct Feed: Decodable, Sendable {
public let title: String
public let feedId: String
}
let results: [Feed]
public let results: [Feed]
}

View File

@@ -8,11 +8,12 @@
import Foundation
struct FeedlyLink: Decodable {
let href: String
public struct FeedlyLink: Decodable, Sendable {
public let href: String
/// The mime type of the resource located by `href`.
/// When `nil`, it's probably a web page?
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
let type: String?
public let type: String?
}

View File

@@ -8,8 +8,9 @@
import Foundation
struct FeedlyOrigin: Decodable {
let title: String?
let streamID: String?
let htmlUrl: String?
public struct FeedlyOrigin: Decodable, Sendable {
public let title: String?
public let streamID: String?
public let htmlUrl: String?
}

View File

@@ -8,11 +8,12 @@
import Foundation
struct FeedlyRTLTextSanitizer {
public struct FeedlyRTLTextSanitizer: Sendable {
private let rightToLeftPrefix = "<div style=\"direction:rtl;text-align:right\">"
private let rightToLeftSuffix = "</div>"
func sanitize(_ sourceText: String?) -> String? {
public func sanitize(_ sourceText: String?) -> String? {
guard let source = sourceText, !source.isEmpty else {
return sourceText
}

View File

@@ -9,16 +9,17 @@
import Foundation
/// The kinds of Resource IDs is documented here: https://developer.feedly.com/cloud/
protocol FeedlyResourceID {
public protocol FeedlyResourceID {
/// The resource ID from Feedly.
@MainActor var id: String { get }
}
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
struct FeedlyFeedResourceID: FeedlyResourceID {
let id: String
public struct FeedlyFeedResourceID: FeedlyResourceID, Sendable {
public let id: String
/// The location of the kind of resource a concrete type represents.
/// If the concrete type cannot strip the resource type from the ID, it should just return the ID
/// since the ID is a legitimate URL.
@@ -26,7 +27,7 @@ struct FeedlyFeedResourceID: FeedlyResourceID {
/// 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 {
public var url: String {
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
var mutant = id
mutant.removeSubrange(range)
@@ -36,34 +37,40 @@ struct FeedlyFeedResourceID: FeedlyResourceID {
// It seems values like "something/https://my.blog/posts.xml" is a legit URL.
return id
}
public init(id: String) {
self.id = id
}
}
extension FeedlyFeedResourceID {
init(url: String) {
self.id = "feed/\(url)"
}
}
struct FeedlyCategoryResourceID: FeedlyResourceID {
let id: String
enum Global {
static func uncategorized(for userID: String) -> FeedlyCategoryResourceID {
public struct FeedlyCategoryResourceID: FeedlyResourceID, Sendable {
public let id: String
public enum Global {
public 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)
}
/// All articles from all the feeds the user subscribes to.
static func all(for userID: String) -> FeedlyCategoryResourceID {
public static func all(for userID: String) -> FeedlyCategoryResourceID {
// https://developer.feedly.com/cloud/#global-resource-ids
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 {
public 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)
@@ -71,12 +78,13 @@ struct FeedlyCategoryResourceID: FeedlyResourceID {
}
}
struct FeedlyTagResourceID: FeedlyResourceID {
let id: String
enum Global {
static func saved(for userID: String) -> FeedlyTagResourceID {
public struct FeedlyTagResourceID: FeedlyResourceID, Sendable {
public let id: String
public enum Global {
public static func saved(for userID: String) -> FeedlyTagResourceID {
// https://developer.feedly.com/cloud/#global-resource-ids
let id = "user/\(userID)/tag/global.saved"
return FeedlyTagResourceID(id: id)

View File

@@ -8,19 +8,20 @@
import Foundation
struct FeedlyStream: Decodable {
let id: String
public struct FeedlyStream: Decodable, Sendable {
public let id: String
/// Of the most recent entry for this stream (regardless of continuation, newerThan, etc).
let updated: Date?
public let updated: Date?
/// the continuation id to pass to the next stream call, for pagination.
/// This id guarantees that no entry will be duplicated in a stream (meaning, there is no need to de-duplicate entries returned by this call).
/// If this value is not returned, it means the end of the stream has been reached.
let continuation: String?
let items: [FeedlyEntry]
var isStreamEnd: Bool {
public let continuation: String?
public let items: [FeedlyEntry]
public var isStreamEnd: Bool {
return continuation == nil
}
}

View File

@@ -8,11 +8,12 @@
import Foundation
struct FeedlyStreamIDs: Decodable {
let continuation: String?
let ids: [String]
var isStreamEnd: Bool {
public struct FeedlyStreamIDs: Decodable, Sendable {
public let continuation: String?
public let ids: [String]
public var isStreamEnd: Bool {
return continuation == nil
}
}

View File

@@ -8,7 +8,8 @@
import Foundation
struct FeedlyTag: Decodable {
let id: String
let label: String?
public struct FeedlyTag: Decodable, Sendable {
public let id: String
public let label: String?
}

View File

@@ -9,25 +9,25 @@
import Foundation
import os.log
protocol FeedlyCollectionProviding: AnyObject {
public protocol FeedlyCollectionProviding: AnyObject {
@MainActor var collections: [FeedlyCollection] { get }
}
/// Get Collections from Feedly.
final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding {
public final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding {
let service: FeedlyGetCollectionsService
let log: OSLog
private(set) var collections = [FeedlyCollection]()
private(set) public var collections = [FeedlyCollection]()
init(service: FeedlyGetCollectionsService, log: OSLog) {
public init(service: FeedlyGetCollectionsService, log: OSLog) {
self.service = service
self.log = log
}
override func run() {
public override func run() {
os_log(.debug, log: log, "Requesting collections.")
service.getCollections { result in

View File

@@ -11,23 +11,23 @@ import os.log
import Parser
/// Get full entries for the entry identifiers.
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
public final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
let service: FeedlyGetEntriesService
let provider: FeedlyEntryIdentifierProviding
let log: OSLog
init(service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) {
public init(service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) {
self.service = service
self.provider = provider
self.log = log
}
private (set) var entries = [FeedlyEntry]()
private (set) public var entries = [FeedlyEntry]()
private var storedParsedEntries: Set<ParsedItem>?
var parsedEntries: Set<ParsedItem> {
public var parsedEntries: Set<ParsedItem> {
if let entries = storedParsedEntries {
return entries
}
@@ -49,11 +49,11 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, Fe
return parsed
}
var parsedItemProviderName: String {
public var parsedItemProviderName: String {
return name ?? String(describing: Self.self)
}
override func run() {
public override func run() {
service.getEntries(for: provider.entryIDs) { result in
switch result {
case .success(let entries):

View File

@@ -10,33 +10,33 @@ import Foundation
import Parser
import os.log
protocol FeedlyEntryProviding {
public protocol FeedlyEntryProviding {
@MainActor var entries: [FeedlyEntry] { get }
}
protocol FeedlyParsedItemProviding {
public protocol FeedlyParsedItemProviding {
@MainActor var parsedItemProviderName: String { get }
@MainActor var parsedEntries: Set<ParsedItem> { get }
}
protocol FeedlyGetStreamContentsOperationDelegate: AnyObject {
public protocol FeedlyGetStreamContentsOperationDelegate: AnyObject {
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream)
}
/// Get the stream content of a Collection from Feedly.
final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
public final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
@MainActor struct ResourceProvider: FeedlyResourceProviding {
var resource: FeedlyResourceID
}
let resourceProvider: FeedlyResourceProviding
var parsedItemProviderName: String {
public var parsedItemProviderName: String {
return resourceProvider.resource.id
}
var entries: [FeedlyEntry] {
public var entries: [FeedlyEntry] {
guard let entries = stream?.items else {
// assert(isFinished, "This should only be called when the operation finishes without error.")
assertionFailure("Has this operation been addeded as a dependency on the caller?")
@@ -45,7 +45,7 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
return entries
}
var parsedEntries: Set<ParsedItem> {
public var parsedEntries: Set<ParsedItem> {
if let entries = storedParsedEntries {
return entries
}
@@ -74,17 +74,16 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
private var storedParsedEntries: Set<ParsedItem>?
let account: Account
let service: FeedlyGetStreamContentsService
let unreadOnly: Bool?
let newerThan: Date?
let continuation: String?
let log: OSLog
weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate?
public weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate?
public init(resource: FeedlyResourceID, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
self.account = account
self.resourceProvider = ResourceProvider(resource: resource)
self.service = service
self.continuation = continuation
@@ -93,11 +92,12 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
self.log = log
}
convenience init(account: Account, resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
self.init(account: account, resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly, log: log)
convenience init(resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
self.init(resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly, log: log)
}
override func run() {
public override func run() {
service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
switch result {
case .success(let stream):

View File

@@ -9,14 +9,14 @@
import Foundation
import os.log
protocol FeedlyGetStreamIDsOperationDelegate: AnyObject {
public protocol FeedlyGetStreamIDsOperationDelegate: AnyObject {
func feedlyGetStreamIDsOperation(_ operation: FeedlyGetStreamIDsOperation, didGet streamIDs: FeedlyStreamIDs)
}
/// Single responsibility is to get the stream ids from Feedly.
final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
public final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
var entryIDs: Set<String> {
public var entryIDs: Set<String> {
guard let ids = streamIDs?.ids else {
assertionFailure("Has this operation been addeded as a dependency on the caller?")
return []
@@ -26,7 +26,6 @@ final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierP
private(set) var streamIDs: FeedlyStreamIDs?
let account: Account
let service: FeedlyGetStreamIDsService
let continuation: String?
let resource: FeedlyResourceID
@@ -34,8 +33,8 @@ final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierP
let newerThan: Date?
let log: OSLog
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, continuation: String? = nil, newerThan: Date? = nil, unreadOnly: Bool?, log: OSLog) {
self.account = account
init(resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, continuation: String? = nil, newerThan: Date? = nil, unreadOnly: Bool?, log: OSLog) {
self.resource = resource
self.service = service
self.continuation = continuation
@@ -46,7 +45,7 @@ final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierP
weak var streamIDsDelegate: FeedlyGetStreamIDsOperationDelegate?
override func run() {
public override func run() {
service.getStreamIDs(for: resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
switch result {
case .success(let stream):

View File

@@ -14,34 +14,33 @@ import Secrets
///
/// Typically, it pages through the article ids of the global.all stream.
/// When all the article ids are collected, it is the responsibility of another operation to download them when appropriate.
class FeedlyGetUpdatedArticleIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
public final class FeedlyGetUpdatedArticleIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
private let account: Account
private let resource: FeedlyResourceID
private let service: FeedlyGetStreamIDsService
private let newerThan: Date?
private let log: OSLog
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
self.account = account
public init(resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
self.resource = resource
self.service = service
self.newerThan = newerThan
self.log = log
}
convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
public convenience init(userID: String, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
let all = FeedlyCategoryResourceID.Global.all(for: userID)
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
self.init(resource: all, service: service, newerThan: newerThan, log: log)
}
var entryIDs: Set<String> {
public var entryIDs: Set<String> {
return storedUpdatedArticleIDs
}
private var storedUpdatedArticleIDs = Set<String>()
override func run() {
public override func run() {
getStreamIDs(nil)
}

View File

@@ -9,24 +9,22 @@
import Foundation
import os.log
protocol FeedlyLogoutService {
public protocol FeedlyLogoutService {
func logout(completion: @escaping (Result<Void, Error>) -> ())
}
final class FeedlyLogoutOperation: FeedlyOperation {
public final class FeedlyLogoutOperation: FeedlyOperation {
let service: FeedlyLogoutService
let account: Account
let log: OSLog
init(account: Account, service: FeedlyLogoutService, log: OSLog) {
public init(service: FeedlyLogoutService, log: OSLog) {
self.service = service
self.account = account
self.log = log
}
override func run() {
os_log("Requesting logout of %{public}@ account.", "\(account.type)")
public override func run() {
os_log("Requesting logout of Feedly account.")
service.logout(completion: didCompleteLogout(_:))
}
@@ -34,10 +32,11 @@ final class FeedlyLogoutOperation: FeedlyOperation {
assert(Thread.isMainThread)
switch result {
case .success:
os_log("Logged out of %{public}@ account.", "\(account.type)")
os_log("Logged out of Feedly account.")
do {
try account.removeCredentials(type: .oauthAccessToken)
try account.removeCredentials(type: .oauthRefreshToken)
// TODO: fix removing credentials
// try account.removeCredentials(type: .oauthAccessToken)
// try account.removeCredentials(type: .oauthRefreshToken)
} catch {
// oh well, we tried our best.
}

View File

@@ -10,7 +10,7 @@ import Foundation
import Web
import Core
protocol FeedlyOperationDelegate: AnyObject {
public protocol FeedlyOperationDelegate: AnyObject {
@MainActor func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error)
}
@@ -18,10 +18,10 @@ protocol FeedlyOperationDelegate: AnyObject {
///
/// Normally we dont do inheritance but in this case
/// its the best option.
@MainActor class FeedlyOperation: MainThreadOperation {
@MainActor open class FeedlyOperation: MainThreadOperation {
weak var delegate: FeedlyOperationDelegate?
var downloadProgress: DownloadProgress? {
public weak var delegate: FeedlyOperationDelegate?
public var downloadProgress: DownloadProgress? {
didSet {
oldValue?.completeTask()
downloadProgress?.addToNumberOfTasksAndRemaining(1)
@@ -29,34 +29,36 @@ protocol FeedlyOperationDelegate: AnyObject {
}
// MainThreadOperation
var isCanceled = false {
public var isCanceled = false {
didSet {
if isCanceled {
didCancel()
}
}
}
var id: Int?
weak var operationDelegate: MainThreadOperationDelegate?
var name: String?
var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
public var id: Int?
public weak var operationDelegate: MainThreadOperationDelegate?
public var name: String?
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
func run() {
public init() {}
open func run() {
}
func didFinish() {
open func didFinish() {
if !isCanceled {
operationDelegate?.operationDidComplete(self)
}
downloadProgress?.completeTask()
}
func didFinish(with error: Error) {
open func didFinish(with error: Error) {
delegate?.feedlyOperation(self, didFailWith: error)
didFinish()
}
func didCancel() {
open func didCancel() {
didFinish()
}
}

View File

@@ -10,36 +10,36 @@ import Foundation
import Parser
import os.log
protocol FeedlyParsedItemsByFeedProviding {
var parsedItemsByFeedProviderName: String { get }
var parsedItemsKeyedByFeedID: [String: Set<ParsedItem>] { get }
public protocol FeedlyParsedItemsByFeedProviding {
@MainActor var parsedItemsByFeedProviderName: String { get }
@MainActor var parsedItemsKeyedByFeedID: [String: Set<ParsedItem>] { get }
}
/// Group articles by their feeds.
final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding {
public final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding {
private let account: Account
private let parsedItemProvider: FeedlyParsedItemProviding
private let log: OSLog
var parsedItemsByFeedProviderName: String {
public var parsedItemsByFeedProviderName: String {
return name ?? String(describing: Self.self)
}
var parsedItemsKeyedByFeedID: [String : Set<ParsedItem>] {
public var parsedItemsKeyedByFeedID: [String : Set<ParsedItem>] {
precondition(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type.
return itemsKeyedByFeedID
}
private var itemsKeyedByFeedID = [String: Set<ParsedItem>]()
init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
self.account = account
public init(parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
self.parsedItemProvider = parsedItemProvider
self.log = log
}
override func run() {
public override func run() {
defer {
didFinish()
}

View File

@@ -15,19 +15,18 @@ protocol FeedlyRequestStreamsOperationDelegate: AnyObject {
/// Create one stream request operation for one Feedly collection.
/// This is the start of the process of refreshing the entire contents of a Folder.
final class FeedlyRequestStreamsOperation: FeedlyOperation {
public final class FeedlyRequestStreamsOperation: FeedlyOperation {
weak var queueDelegate: FeedlyRequestStreamsOperationDelegate?
let collectionsProvider: FeedlyCollectionProviding
let service: FeedlyGetStreamContentsService
let account: Account
let log: OSLog
let newerThan: Date?
let unreadOnly: Bool?
init(account: Account, collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, service: FeedlyGetStreamContentsService, log: OSLog) {
self.account = account
init(collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, service: FeedlyGetStreamContentsService, log: OSLog) {
self.service = service
self.collectionsProvider = collectionsProvider
self.newerThan = newerThan
@@ -35,7 +34,7 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
self.log = log
}
override func run() {
public override func run() {
defer {
didFinish()
}
@@ -46,12 +45,11 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
for collection in collectionsProvider.collections {
let resource = FeedlyCategoryResourceID(id: collection.id)
let operation = FeedlyGetStreamContentsOperation(account: account,
resource: resource,
service: service,
newerThan: newerThan,
unreadOnly: unreadOnly,
log: log)
let operation = FeedlyGetStreamContentsOperation(resource: resource,
service: service,
newerThan: newerThan,
unreadOnly: unreadOnly,
log: log)
queueDelegate?.feedlyRequestStreamsOperation(self, enqueue: operation)
}

View File

@@ -8,30 +8,30 @@
import Foundation
protocol FeedlySearchService: AnyObject {
public protocol FeedlySearchService: AnyObject {
func getFeeds(for query: String, count: Int, locale: String, completion: @escaping (Result<FeedlyFeedsSearchResponse, Error>) -> ())
}
protocol FeedlySearchOperationDelegate: AnyObject {
public protocol FeedlySearchOperationDelegate: AnyObject {
@MainActor func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse)
}
/// 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 {
public class FeedlySearchOperation: FeedlyOperation {
let query: String
let locale: Locale
let searchService: FeedlySearchService
weak var searchDelegate: FeedlySearchOperationDelegate?
public weak var searchDelegate: FeedlySearchOperationDelegate?
init(query: String, locale: Locale = .current, service: FeedlySearchService) {
public init(query: String, locale: Locale = .current, service: FeedlySearchService) {
self.query = query
self.locale = locale
self.searchService = service
}
override func run() {
public override func run() {
searchService.getFeeds(for: query, count: 1, locale: locale.identifier) { result in
switch result {
case .success(let response):

View File

@@ -11,21 +11,20 @@ import Articles
import SyncDatabase
import os.log
/// Take changes to statuses of articles locally and apply them to the corresponding the articles remotely.
final class FeedlySendArticleStatusesOperation: FeedlyOperation {
public final class FeedlySendArticleStatusesOperation: FeedlyOperation {
private let database: SyncDatabase
private let log: OSLog
private let service: FeedlyMarkArticlesService
init(database: SyncDatabase, service: FeedlyMarkArticlesService, log: OSLog) {
public init(database: SyncDatabase, service: FeedlyMarkArticlesService, log: OSLog) {
self.database = database
self.service = service
self.log = log
}
override func run() {
public override func run() {
os_log(.debug, log: log, "Sending article statuses...")
Task { @MainActor in

View File

@@ -8,6 +8,6 @@
import Foundation
protocol FeedlyGetCollectionsService: AnyObject {
public protocol FeedlyGetCollectionsService: AnyObject {
func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ())
}

View File

@@ -8,6 +8,6 @@
import Foundation
protocol FeedlyGetEntriesService: AnyObject {
public protocol FeedlyGetEntriesService: AnyObject {
func getEntries(for ids: Set<String>, completion: @escaping (Result<[FeedlyEntry], Error>) -> ())
}

View File

@@ -8,6 +8,6 @@
import Foundation
protocol FeedlyGetStreamContentsService: AnyObject {
public protocol FeedlyGetStreamContentsService: AnyObject {
func getStreamContents(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result<FeedlyStream, Error>) -> ())
}

View File

@@ -8,6 +8,6 @@
import Foundation
protocol FeedlyGetStreamIDsService: AnyObject {
public protocol FeedlyGetStreamIDsService: AnyObject {
func getStreamIDs(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result<FeedlyStreamIDs, Error>) -> ())
}

View File

@@ -8,28 +8,29 @@
import Foundation
enum FeedlyMarkAction: String {
case read
case unread
case saved
case unsaved
public enum FeedlyMarkAction: String, Sendable {
case read
case unread
case saved
case unsaved
/// These values are paired with the "action" key in POST requests to the markers API.
/// See for example: https://developer.feedly.com/v3/markers/#mark-one-or-multiple-articles-as-read
var actionValue: String {
switch self {
case .read:
return "markAsRead"
case .unread:
return "keepUnread"
case .saved:
return "markAsSaved"
case .unsaved:
return "markAsUnsaved"
}
}
}
public var actionValue: String {
switch self {
case .read:
return "markAsRead"
case .unread:
return "keepUnread"
case .saved:
return "markAsSaved"
case .unsaved:
return "markAsUnsaved"
}
}
}
protocol FeedlyMarkArticlesService: AnyObject {
public protocol FeedlyMarkArticlesService: AnyObject {
func mark(_ articleIDs: Set<String>, as action: FeedlyMarkAction, completion: @escaping (Result<Void, Error>) -> ())
}

View File

@@ -0,0 +1,12 @@
import XCTest
@testable import Feedly
final class FeedlyTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}

View File

@@ -1392,6 +1392,7 @@
84A3EE52223B667F00557320 /* DefaultFeeds.opml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = DefaultFeeds.opml; sourceTree = "<group>"; };
84A699132BC34E8500605AB8 /* ArticleExtractor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ArticleExtractor; sourceTree = "<group>"; };
84A699182BC3524C00605AB8 /* LocalAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LocalAccount; sourceTree = "<group>"; };
84A699192BC36EDB00605AB8 /* Feedly */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Feedly; sourceTree = "<group>"; };
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardFolder.swift; sourceTree = "<group>"; };
84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedPasteboardWriter.swift; sourceTree = "<group>"; };
84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOutlineDataSource.swift; sourceTree = "<group>"; };
@@ -2353,6 +2354,7 @@
84FB9FAE2BC3494B00B7AFC3 /* FeedFinder */,
84A699182BC3524C00605AB8 /* LocalAccount */,
84FB9FAD2BC344F800B7AFC3 /* Feedbin */,
84A699192BC36EDB00605AB8 /* Feedly */,
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */,
84CC98D92BC1DD25006A05C9 /* ReaderAPI */,
845F3D2B2BC268FE00AEBB68 /* CloudKitSync */,