Merge branch 'master' into feature/feed-wrangler
# Conflicts: # Frameworks/Account/Account.swift # Frameworks/Account/Account.xcodeproj/project.pbxproj # NetNewsWire.xcodeproj/project.pbxproj # submodules/RSCore
This commit is contained in:
commit
b4a862d207
|
@ -49,4 +49,14 @@ jobs:
|
|||
SCHEME: ${{ matrix.run-config['scheme'] }}
|
||||
DESTINATION: ${{ matrix.run-config['destination'] }}
|
||||
|
||||
run: buildscripts/ci-build.sh
|
||||
run: buildscripts/ci-build.sh
|
||||
|
||||
- name: Notify Slack
|
||||
uses: 8398a7/action-slack@v2.4.2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
author_name: GitHub Actions CI
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure()
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<channel>
|
||||
<title>NetNewsWire Betas</title>
|
||||
<link>https://ranchero.com/downloads/netnewswire-beta.xml</link>
|
||||
<description>Most recent NetNewsWire changes with links to updates.</description>
|
||||
<language>en</language>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 5.1d2</title>
|
||||
<description><![CDATA[
|
||||
<p>Testing Sparkle. Does it work?</p>
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Tue, 29 Oct 2019 22:35:00 -0700</pubDate>
|
||||
<enclosure url="https://ranchero.com/downloads/NetNewsWire5.1d2.zip" sparkle:version="3002" sparkle:shortVersionString="5.1d2" length="5391151" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
|
@ -36,7 +36,7 @@ set totalFeeds to 0
|
|||
tell application "NetNewsWire"
|
||||
set allAccounts to every account
|
||||
repeat with nthAccount in allAccounts
|
||||
set allFeeds to every feed of nthAccount
|
||||
set allFeeds to every webFeed of nthAccount
|
||||
repeat with nthFeed in allFeeds
|
||||
set feedname to name of nthFeed
|
||||
set articleCount to count (get every article of nthFeed)
|
||||
|
|
|
@ -38,7 +38,7 @@ set safariWindow to missing value
|
|||
tell application "NetNewsWire"
|
||||
set allAccounts to every account
|
||||
repeat with nthAccount in allAccounts
|
||||
set allFeeds to every feed of nthAccount
|
||||
set allFeeds to every webFeed of nthAccount
|
||||
repeat with nthFeed in allFeeds
|
||||
set starredArticles to (get every article of nthFeed where starred is true)
|
||||
repeat with nthArticle in starredArticles
|
||||
|
|
|
@ -29,7 +29,7 @@ public extension Notification.Name {
|
|||
static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
|
||||
static let AccountStateDidChange = Notification.Name(rawValue: "AccountStateDidChange")
|
||||
static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
|
||||
static let FeedMetadataDidChange = Notification.Name(rawValue: "FeedMetadataDidChange")
|
||||
static let WebFeedMetadataDidChange = Notification.Name(rawValue: "WebFeedMetadataDidChange")
|
||||
}
|
||||
|
||||
public enum AccountType: Int {
|
||||
|
@ -48,7 +48,7 @@ public enum FetchType {
|
|||
case unread
|
||||
case today
|
||||
case unreadForFolder(Folder)
|
||||
case feed(Feed)
|
||||
case webFeed(WebFeed)
|
||||
case articleIDs(Set<String>)
|
||||
case search(String)
|
||||
case searchWithArticleIDs(String, Set<String>)
|
||||
|
@ -62,7 +62,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
|
||||
public static let statuses = "statuses" // StatusesDidChange
|
||||
public static let articles = "articles" // StatusesDidChange
|
||||
public static let feeds = "feeds" // AccountDidDownloadArticles, StatusesDidChange
|
||||
public static let webFeeds = "webFeeds" // AccountDidDownloadArticles, StatusesDidChange
|
||||
}
|
||||
|
||||
public static let defaultLocalAccountName: String = {
|
||||
|
@ -126,15 +126,23 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
}
|
||||
}
|
||||
|
||||
public var topLevelFeeds = Set<Feed>()
|
||||
public var topLevelWebFeeds = Set<WebFeed>()
|
||||
public var folders: Set<Folder>? = Set<Folder>()
|
||||
private var feedDictionaryNeedsUpdate = true
|
||||
private var _idToFeedDictionary = [String: Feed]()
|
||||
var idToFeedDictionary: [String: Feed] {
|
||||
if feedDictionaryNeedsUpdate {
|
||||
rebuildFeedDictionaries()
|
||||
|
||||
public var sortedFolders: [Folder]? {
|
||||
if let folders = folders {
|
||||
return Array(folders).sorted(by: { $0.nameForDisplay < $1.nameForDisplay })
|
||||
}
|
||||
return _idToFeedDictionary
|
||||
return nil
|
||||
}
|
||||
|
||||
private var webFeedDictionaryNeedsUpdate = true
|
||||
private var _idToWebFeedDictionary = [String: WebFeed]()
|
||||
var idToWebFeedDictionary: [String: WebFeed] {
|
||||
if webFeedDictionaryNeedsUpdate {
|
||||
rebuildWebFeedDictionaries()
|
||||
}
|
||||
return _idToWebFeedDictionary
|
||||
}
|
||||
|
||||
var username: String? {
|
||||
|
@ -169,8 +177,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
private var unreadCounts = [String: Int]() // [feedID: Int]
|
||||
|
||||
private var _flattenedFeeds = Set<Feed>()
|
||||
private var flattenedFeedsNeedUpdate = true
|
||||
private var _flattenedWebFeeds = Set<WebFeed>()
|
||||
private var flattenedWebFeedsNeedUpdate = true
|
||||
|
||||
private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self)
|
||||
private lazy var metadataFile = AccountMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("Settings.plist"), account: self)
|
||||
|
@ -180,9 +188,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
}
|
||||
}
|
||||
|
||||
private lazy var feedMetadataFile = FeedMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist"), account: self)
|
||||
typealias FeedMetadataDictionary = [String: FeedMetadata]
|
||||
var feedMetadata = FeedMetadataDictionary()
|
||||
private lazy var webFeedMetadataFile = WebFeedMetadataFile(filename: (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist"), account: self)
|
||||
typealias WebFeedMetadataDictionary = [String: WebFeedMetadata]
|
||||
var webFeedMetadata = WebFeedMetadataDictionary()
|
||||
|
||||
var startingUp = true
|
||||
|
||||
|
@ -225,9 +233,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
case .freshRSS:
|
||||
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .feedly:
|
||||
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment)
|
||||
case .feedWrangler:
|
||||
self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -261,10 +270,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||
|
||||
metadataFile.load()
|
||||
feedMetadataFile.load()
|
||||
webFeedMetadataFile.load()
|
||||
opmlFile.load()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.database.cleanupDatabaseAtStartup(subscribedToWebFeedIDs: self.flattenedWebFeeds().webFeedIDs())
|
||||
self.fetchAllUnreadCounts()
|
||||
}
|
||||
|
||||
|
@ -311,7 +321,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
}
|
||||
}
|
||||
|
||||
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType, client: OAuthAuthorizationClient) -> URLRequest {
|
||||
internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient {
|
||||
switch type {
|
||||
case .feedly:
|
||||
return FeedlyAccountDelegate.environment.oauthAuthorizationClient
|
||||
default:
|
||||
fatalError("\(type) is not a client for OAuth authorization code granting.")
|
||||
}
|
||||
}
|
||||
|
||||
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest {
|
||||
let grantingType: OAuthAuthorizationGranting.Type
|
||||
switch type {
|
||||
case .feedly:
|
||||
|
@ -320,7 +339,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
fatalError("\(type) does not support OAuth authorization code granting.")
|
||||
}
|
||||
|
||||
return grantingType.oauthAuthorizationCodeGrantRequest(for: client)
|
||||
return grantingType.oauthAuthorizationCodeGrantRequest()
|
||||
}
|
||||
|
||||
public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse,
|
||||
|
@ -337,17 +356,27 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
fatalError("\(accountType) does not support OAuth authorization code granting.")
|
||||
}
|
||||
|
||||
grantingType.requestOAuthAccessToken(with: response, client: client, transport: transport, completionHandler: completionHandler)
|
||||
grantingType.requestOAuthAccessToken(with: response, transport: transport, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func refreshAll(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
self.delegate.refreshAll(for: self, completion: completion)
|
||||
}
|
||||
|
||||
public func syncArticleStatus(completion: (() -> Void)? = nil) {
|
||||
delegate.sendArticleStatus(for: self) { [unowned self] in
|
||||
self.delegate.refreshArticleStatus(for: self) {
|
||||
completion?()
|
||||
public func syncArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
delegate.sendArticleStatus(for: self) { [unowned self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.delegate.refreshArticleStatus(for: self) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -372,19 +401,28 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
}
|
||||
|
||||
public func suspend() {
|
||||
delegate.cancelAll(for: self)
|
||||
save()
|
||||
}
|
||||
|
||||
public func save() {
|
||||
metadataFile.save()
|
||||
feedMetadataFile.save()
|
||||
webFeedMetadataFile.save()
|
||||
opmlFile.save()
|
||||
}
|
||||
|
||||
public func prepareForDeletion() {
|
||||
delegate.accountWillBeDeleted(self)
|
||||
}
|
||||
|
||||
func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) {
|
||||
var feedsToAdd = Set<Feed>()
|
||||
var feedsToAdd = Set<WebFeed>()
|
||||
|
||||
items.forEach { (item) in
|
||||
|
||||
if let feedSpecifier = item.feedSpecifier {
|
||||
let feed = newFeed(with: feedSpecifier)
|
||||
let feed = newWebFeed(with: feedSpecifier)
|
||||
feedsToAdd.insert(feed)
|
||||
return
|
||||
}
|
||||
|
@ -407,22 +445,22 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
if let parentFolder = parentFolder {
|
||||
for feed in feedsToAdd {
|
||||
parentFolder.addFeed(feed)
|
||||
parentFolder.addWebFeed(feed)
|
||||
}
|
||||
} else {
|
||||
for feed in feedsToAdd {
|
||||
addFeed(feed)
|
||||
addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func resetFeedMetadataAndUnreadCounts() {
|
||||
for feed in flattenedFeeds() {
|
||||
feed.metadata = feedMetadata(feedURL: feed.url, feedID: feed.feedID)
|
||||
public func resetWebFeedMetadataAndUnreadCounts() {
|
||||
for feed in flattenedWebFeeds() {
|
||||
feed.metadata = webFeedMetadata(feedURL: feed.url, webFeedID: feed.webFeedID)
|
||||
}
|
||||
fetchAllUnreadCounts()
|
||||
NotificationCenter.default.post(name: .FeedMetadataDidChange, object: self, userInfo: nil)
|
||||
NotificationCenter.default.post(name: .WebFeedMetadataDidChange, object: self, userInfo: nil)
|
||||
}
|
||||
|
||||
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
|
@ -463,10 +501,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return folders?.first(where: { $0.nameForDisplay == displayName })
|
||||
}
|
||||
|
||||
func newFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
|
||||
func newWebFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> WebFeed {
|
||||
let feedURL = opmlFeedSpecifier.feedURL
|
||||
let metadata = feedMetadata(feedURL: feedURL, feedID: feedURL)
|
||||
let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
|
||||
let metadata = webFeedMetadata(feedURL: feedURL, webFeedID: feedURL)
|
||||
let feed = WebFeed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
|
||||
if let feedTitle = opmlFeedSpecifier.title {
|
||||
if feed.name == nil {
|
||||
feed.name = feedTitle
|
||||
|
@ -475,37 +513,37 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return feed
|
||||
}
|
||||
|
||||
public func addFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.addFeed(for: self, with: feed, to: container, completion: completion)
|
||||
public func addWebFeed(_ feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.addWebFeed(for: self, with: feed, to: container, completion: completion)
|
||||
}
|
||||
|
||||
public func createFeed(url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
delegate.createFeed(for: self, url: url, name: name, container: container, completion: completion)
|
||||
public func createWebFeed(url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
delegate.createWebFeed(for: self, url: url, name: name, container: container, completion: completion)
|
||||
}
|
||||
|
||||
func createFeed(with name: String?, url: String, feedID: String, homePageURL: String?) -> Feed {
|
||||
let metadata = feedMetadata(feedURL: url, feedID: feedID)
|
||||
let feed = Feed(account: self, url: url, metadata: metadata)
|
||||
func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> WebFeed {
|
||||
let metadata = webFeedMetadata(feedURL: url, webFeedID: webFeedID)
|
||||
let feed = WebFeed(account: self, url: url, metadata: metadata)
|
||||
feed.name = name
|
||||
feed.homePageURL = homePageURL
|
||||
|
||||
return feed
|
||||
}
|
||||
|
||||
public func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeFeed(for: self, with: feed, from: container, completion: completion)
|
||||
public func removeWebFeed(_ feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeWebFeed(for: self, with: feed, from: container, completion: completion)
|
||||
}
|
||||
|
||||
public func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.moveFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
||||
public func moveWebFeed(_ feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.moveWebFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
||||
}
|
||||
|
||||
public func renameFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.renameFeed(for: self, with: feed, to: name, completion: completion)
|
||||
public func renameWebFeed(_ feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.renameWebFeed(for: self, with: feed, to: name, completion: completion)
|
||||
}
|
||||
|
||||
public func restoreFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion)
|
||||
public func restoreWebFeed(_ feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.restoreWebFeed(for: self, feed: feed, container: container, completion: completion)
|
||||
}
|
||||
|
||||
public func addFolder(_ name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
|
@ -524,8 +562,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
||||
}
|
||||
|
||||
func clearFeedMetadata(_ feed: Feed) {
|
||||
feedMetadata[feed.url] = nil
|
||||
func clearWebFeedMetadata(_ feed: WebFeed) {
|
||||
webFeedMetadata[feed.url] = nil
|
||||
}
|
||||
|
||||
func addFolder(_ folder: Folder) {
|
||||
|
@ -534,15 +572,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
structureDidChange()
|
||||
}
|
||||
|
||||
public func updateUnreadCounts(for feeds: Set<Feed>) {
|
||||
if feeds.isEmpty {
|
||||
public func updateUnreadCounts(for webFeeds: Set<WebFeed>) {
|
||||
if webFeeds.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
database.fetchUnreadCounts(for: feeds.feedIDs()) { (unreadCountDictionary) in
|
||||
for feed in feeds {
|
||||
if let unreadCount = unreadCountDictionary[feed.feedID] {
|
||||
feed.unreadCount = unreadCount
|
||||
database.fetchUnreadCounts(for: webFeeds.webFeedIDs()) { (unreadCountDictionary) in
|
||||
for webFeed in webFeeds {
|
||||
if let unreadCount = unreadCountDictionary[webFeed.webFeedID] {
|
||||
webFeed.unreadCount = unreadCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -558,8 +596,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return fetchTodayArticles()
|
||||
case .unreadForFolder(let folder):
|
||||
return fetchArticles(folder: folder)
|
||||
case .feed(let feed):
|
||||
return fetchArticles(feed: feed)
|
||||
case .webFeed(let webFeed):
|
||||
return fetchArticles(webFeed: webFeed)
|
||||
case .articleIDs(let articleIDs):
|
||||
return fetchArticles(articleIDs: articleIDs)
|
||||
case .search(let searchString):
|
||||
|
@ -579,8 +617,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
fetchTodayArticlesAsync(callback)
|
||||
case .unreadForFolder(let folder):
|
||||
fetchArticlesAsync(folder: folder, callback)
|
||||
case .feed(let feed):
|
||||
fetchArticlesAsync(feed: feed, callback)
|
||||
case .webFeed(let webFeed):
|
||||
fetchArticlesAsync(webFeed: webFeed, callback)
|
||||
case .articleIDs(let articleIDs):
|
||||
fetchArticlesAsync(articleIDs: articleIDs, callback)
|
||||
case .search(let searchString):
|
||||
|
@ -591,11 +629,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
}
|
||||
|
||||
public func fetchUnreadCountForToday(_ callback: @escaping (Int) -> Void) {
|
||||
database.fetchUnreadCountForToday(for: flattenedFeeds().feedIDs(), callback: callback)
|
||||
database.fetchUnreadCountForToday(for: flattenedWebFeeds().webFeedIDs(), callback: callback)
|
||||
}
|
||||
|
||||
public func fetchUnreadCountForStarredArticles(_ callback: @escaping (Int) -> Void) {
|
||||
database.fetchStarredAndUnreadCount(for: flattenedFeeds().feedIDs(), callback: callback)
|
||||
database.fetchStarredAndUnreadCount(for: flattenedWebFeeds().webFeedIDs(), callback: callback)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticleIDs() -> Set<String> {
|
||||
|
@ -610,12 +648,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return database.fetchArticleIDsForStatusesWithoutArticles()
|
||||
}
|
||||
|
||||
public func unreadCount(for feed: Feed) -> Int {
|
||||
return unreadCounts[feed.feedID] ?? 0
|
||||
public func unreadCount(for webFeed: WebFeed) -> Int {
|
||||
return unreadCounts[webFeed.webFeedID] ?? 0
|
||||
}
|
||||
|
||||
public func setUnreadCount(_ unreadCount: Int, for feed: Feed) {
|
||||
unreadCounts[feed.feedID] = unreadCount
|
||||
public func setUnreadCount(_ unreadCount: Int, for webFeed: WebFeed) {
|
||||
unreadCounts[webFeed.webFeedID] = unreadCount
|
||||
}
|
||||
|
||||
public func structureDidChange() {
|
||||
|
@ -624,36 +662,36 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
if !startingUp {
|
||||
opmlFile.markAsDirty()
|
||||
}
|
||||
flattenedFeedsNeedUpdate = true
|
||||
feedDictionaryNeedsUpdate = true
|
||||
flattenedWebFeedsNeedUpdate = true
|
||||
webFeedDictionaryNeedsUpdate = true
|
||||
}
|
||||
|
||||
func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping (() -> Void)) {
|
||||
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping (() -> Void)) {
|
||||
// Used only by an On My Mac account.
|
||||
feed.takeSettings(from: parsedFeed)
|
||||
let feedIDsAndItems = [feed.feedID: parsedFeed.items]
|
||||
update(feedIDsAndItems: feedIDsAndItems, defaultRead: false, completion: completion)
|
||||
webFeed.takeSettings(from: parsedFeed)
|
||||
let webFeedIDsAndItems = [webFeed.webFeedID: parsedFeed.items]
|
||||
update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: false, completion: completion)
|
||||
}
|
||||
|
||||
func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping (() -> Void)) {
|
||||
func update(webFeedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping (() -> Void)) {
|
||||
assert(Thread.isMainThread)
|
||||
guard !feedIDsAndItems.isEmpty else {
|
||||
guard !webFeedIDsAndItems.isEmpty else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { (newArticles, updatedArticles) in
|
||||
database.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: defaultRead) { (newArticles, updatedArticles) in
|
||||
var userInfo = [String: Any]()
|
||||
let feeds = Set(feedIDsAndItems.compactMap { (key, _) -> Feed? in
|
||||
self.existingFeed(withFeedID: key)
|
||||
let webFeeds = Set(webFeedIDsAndItems.compactMap { (key, _) -> WebFeed? in
|
||||
self.existingWebFeed(withWebFeedID: key)
|
||||
})
|
||||
if let newArticles = newArticles, !newArticles.isEmpty {
|
||||
self.updateUnreadCounts(for: feeds)
|
||||
self.updateUnreadCounts(for: webFeeds)
|
||||
userInfo[UserInfoKey.newArticles] = newArticles
|
||||
}
|
||||
if let updatedArticles = updatedArticles, !updatedArticles.isEmpty {
|
||||
userInfo[UserInfoKey.updatedArticles] = updatedArticles
|
||||
}
|
||||
userInfo[UserInfoKey.feeds] = feeds
|
||||
userInfo[UserInfoKey.webFeeds] = webFeeds
|
||||
|
||||
completion()
|
||||
|
||||
|
@ -675,10 +713,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
return updatedArticles
|
||||
}
|
||||
|
||||
func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool) {
|
||||
if !articleIDs.isEmpty {
|
||||
database.ensureStatuses(articleIDs, defaultRead, statusKey, flag)
|
||||
func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) {
|
||||
guard !articleIDs.isEmpty else {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
database.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Empty caches that can reasonably be emptied. Call when the app goes in the background, for instance.
|
||||
|
@ -688,38 +728,38 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
// MARK: - Container
|
||||
|
||||
public func flattenedFeeds() -> Set<Feed> {
|
||||
public func flattenedWebFeeds() -> Set<WebFeed> {
|
||||
assert(Thread.isMainThread)
|
||||
if flattenedFeedsNeedUpdate {
|
||||
updateFlattenedFeeds()
|
||||
if flattenedWebFeedsNeedUpdate {
|
||||
updateFlattenedWebFeeds()
|
||||
}
|
||||
return _flattenedFeeds
|
||||
return _flattenedWebFeeds
|
||||
}
|
||||
|
||||
public func removeFeed(_ feed: Feed) {
|
||||
topLevelFeeds.remove(feed)
|
||||
public func removeWebFeed(_ webFeed: WebFeed) {
|
||||
topLevelWebFeeds.remove(webFeed)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
public func removeFeeds(_ webFeeds: Set<WebFeed>) {
|
||||
guard !webFeeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
topLevelFeeds.subtract(feeds)
|
||||
topLevelWebFeeds.subtract(webFeeds)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func addFeed(_ feed: Feed) {
|
||||
topLevelFeeds.insert(feed)
|
||||
public func addWebFeed(_ webFeed: WebFeed) {
|
||||
topLevelWebFeeds.insert(webFeed)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
func addFeedIfNotInAnyFolder(_ feed: Feed) {
|
||||
if !flattenedFeeds().contains(feed) {
|
||||
addFeed(feed)
|
||||
func addFeedIfNotInAnyFolder(_ webFeed: WebFeed) {
|
||||
if !flattenedWebFeeds().contains(webFeed) {
|
||||
addWebFeed(webFeed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -733,7 +773,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
|
||||
public func debugDropConditionalGetInfo() {
|
||||
#if DEBUG
|
||||
flattenedFeeds().forEach{ $0.debugDropConditionalGetInfo() }
|
||||
flattenedWebFeeds().forEach{ $0.debugDropConditionalGetInfo() }
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -759,14 +799,14 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
}
|
||||
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
if let feed = note.object as? Feed, feed.account === self {
|
||||
if let feed = note.object as? WebFeed, feed.account === self {
|
||||
updateUnreadCount()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func batchUpdateDidPerform(_ note: Notification) {
|
||||
flattenedFeedsNeedUpdate = true
|
||||
rebuildFeedDictionaries()
|
||||
flattenedWebFeedsNeedUpdate = true
|
||||
rebuildWebFeedDictionaries()
|
||||
updateUnreadCount()
|
||||
}
|
||||
|
||||
|
@ -812,11 +852,11 @@ extension Account: AccountMetadataDelegate {
|
|||
|
||||
// MARK: - FeedMetadataDelegate
|
||||
|
||||
extension Account: FeedMetadataDelegate {
|
||||
extension Account: WebFeedMetadataDelegate {
|
||||
|
||||
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
|
||||
feedMetadataFile.markAsDirty()
|
||||
guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else {
|
||||
func valueDidChange(_ feedMetadata: WebFeedMetadata, key: WebFeedMetadata.CodingKeys) {
|
||||
webFeedMetadataFile.markAsDirty()
|
||||
guard let feed = existingWebFeed(withWebFeedID: feedMetadata.webFeedID) else {
|
||||
return
|
||||
}
|
||||
feed.postFeedSettingDidChangeNotification(key)
|
||||
|
@ -828,11 +868,11 @@ extension Account: FeedMetadataDelegate {
|
|||
private extension Account {
|
||||
|
||||
func fetchStarredArticles() -> Set<Article> {
|
||||
return database.fetchStarredArticles(flattenedFeeds().feedIDs())
|
||||
return database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs())
|
||||
}
|
||||
|
||||
func fetchStarredArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
database.fetchedStarredArticlesAsync(flattenedFeeds().feedIDs(), callback)
|
||||
database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), callback)
|
||||
}
|
||||
|
||||
func fetchUnreadArticles() -> Set<Article> {
|
||||
|
@ -844,11 +884,11 @@ private extension Account {
|
|||
}
|
||||
|
||||
func fetchTodayArticles() -> Set<Article> {
|
||||
return database.fetchTodayArticles(flattenedFeeds().feedIDs())
|
||||
return database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs())
|
||||
}
|
||||
|
||||
func fetchTodayArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
database.fetchTodayArticlesAsync(flattenedFeeds().feedIDs(), callback)
|
||||
database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), callback)
|
||||
}
|
||||
|
||||
func fetchArticles(folder: Folder) -> Set<Article> {
|
||||
|
@ -859,21 +899,21 @@ private extension Account {
|
|||
fetchUnreadArticlesAsync(forContainer: folder, callback)
|
||||
}
|
||||
|
||||
func fetchArticles(feed: Feed) -> Set<Article> {
|
||||
let articles = database.fetchArticles(feed.feedID)
|
||||
validateUnreadCount(feed, articles)
|
||||
func fetchArticles(webFeed: WebFeed) -> Set<Article> {
|
||||
let articles = database.fetchArticles(webFeed.webFeedID)
|
||||
validateUnreadCount(webFeed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchArticlesAsync(feed: Feed, _ callback: @escaping ArticleSetBlock) {
|
||||
database.fetchArticlesAsync(feed.feedID) { [weak self] (articles) in
|
||||
self?.validateUnreadCount(feed, articles)
|
||||
func fetchArticlesAsync(webFeed: WebFeed, _ callback: @escaping ArticleSetBlock) {
|
||||
database.fetchArticlesAsync(webFeed.webFeedID) { [weak self] (articles) in
|
||||
self?.validateUnreadCount(webFeed, articles)
|
||||
callback(articles)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchArticlesMatching(_ searchString: String) -> Set<Article> {
|
||||
return database.fetchArticlesMatching(searchString, flattenedFeeds().feedIDs())
|
||||
return database.fetchArticlesMatching(searchString, flattenedWebFeeds().webFeedIDs())
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) -> Set<Article> {
|
||||
|
@ -881,7 +921,7 @@ private extension Account {
|
|||
}
|
||||
|
||||
func fetchArticlesMatchingAsync(_ searchString: String, _ callback: @escaping ArticleSetBlock) {
|
||||
database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), callback)
|
||||
database.fetchArticlesMatchingAsync(searchString, flattenedWebFeeds().webFeedIDs(), callback)
|
||||
}
|
||||
|
||||
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
|
||||
|
@ -896,13 +936,13 @@ private extension Account {
|
|||
return database.fetchArticlesAsync(articleIDs: articleIDs, callback)
|
||||
}
|
||||
|
||||
func fetchUnreadArticles(feed: Feed) -> Set<Article> {
|
||||
let articles = database.fetchUnreadArticles(Set([feed.feedID]))
|
||||
validateUnreadCount(feed, articles)
|
||||
func fetchUnreadArticles(webFeed: WebFeed) -> Set<Article> {
|
||||
let articles = database.fetchUnreadArticles(Set([webFeed.webFeedID]))
|
||||
validateUnreadCount(webFeed, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(for feed: Feed, callback: @escaping (Set<Article>) -> Void) {
|
||||
func fetchUnreadArticlesAsync(for webFeed: WebFeed, callback: @escaping (Set<Article>) -> Void) {
|
||||
// database.fetchUnreadArticlesAsync(for: Set([feed.feedID])) { [weak self] (articles) in
|
||||
// self?.validateUnreadCount(feed, articles)
|
||||
// callback(articles)
|
||||
|
@ -911,48 +951,48 @@ private extension Account {
|
|||
|
||||
|
||||
func fetchUnreadArticles(forContainer container: Container) -> Set<Article> {
|
||||
let feeds = container.flattenedFeeds()
|
||||
let articles = database.fetchUnreadArticles(feeds.feedIDs())
|
||||
let feeds = container.flattenedWebFeeds()
|
||||
let articles = database.fetchUnreadArticles(feeds.webFeedIDs())
|
||||
validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
|
||||
return articles
|
||||
}
|
||||
|
||||
func fetchUnreadArticlesAsync(forContainer container: Container, _ callback: @escaping ArticleSetBlock) {
|
||||
let feeds = container.flattenedFeeds()
|
||||
database.fetchUnreadArticlesAsync(feeds.feedIDs()) { [weak self] (articles) in
|
||||
self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
|
||||
let webFeeds = container.flattenedWebFeeds()
|
||||
database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articles) in
|
||||
self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles)
|
||||
callback(articles)
|
||||
}
|
||||
}
|
||||
|
||||
func validateUnreadCountsAfterFetchingUnreadArticles(_ feeds: Set<Feed>, _ articles: Set<Article>) {
|
||||
func validateUnreadCountsAfterFetchingUnreadArticles(_ webFeeds: Set<WebFeed>, _ articles: Set<Article>) {
|
||||
// Validate unread counts. This was the site of a performance slowdown:
|
||||
// it was calling going through the entire list of articles once per feed:
|
||||
// feeds.forEach { validateUnreadCount($0, articles) }
|
||||
// Now we loop through articles exactly once. This makes a huge difference.
|
||||
|
||||
var unreadCountStorage = [String: Int]() // [FeedID: Int]
|
||||
var unreadCountStorage = [String: Int]() // [WebFeedID: Int]
|
||||
for article in articles where !article.status.read {
|
||||
unreadCountStorage[article.feedID, default: 0] += 1
|
||||
unreadCountStorage[article.webFeedID, default: 0] += 1
|
||||
}
|
||||
feeds.forEach { (feed) in
|
||||
let unreadCount = unreadCountStorage[feed.feedID, default: 0]
|
||||
feed.unreadCount = unreadCount
|
||||
webFeeds.forEach { (webFeed) in
|
||||
let unreadCount = unreadCountStorage[webFeed.webFeedID, default: 0]
|
||||
webFeed.unreadCount = unreadCount
|
||||
}
|
||||
}
|
||||
|
||||
func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
||||
func validateUnreadCount(_ webFeed: WebFeed, _ articles: Set<Article>) {
|
||||
// articles must contain all the unread articles for the feed.
|
||||
// The unread number should match the feed’s unread count.
|
||||
|
||||
let feedUnreadCount = articles.reduce(0) { (result, article) -> Int in
|
||||
if article.feed == feed && !article.status.read {
|
||||
if article.webFeed == webFeed && !article.status.read {
|
||||
return result + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
feed.unreadCount = feedUnreadCount
|
||||
webFeed.unreadCount = feedUnreadCount
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -960,37 +1000,37 @@ private extension Account {
|
|||
|
||||
private extension Account {
|
||||
|
||||
func feedMetadata(feedURL: String, feedID: String) -> FeedMetadata {
|
||||
if let d = feedMetadata[feedURL] {
|
||||
func webFeedMetadata(feedURL: String, webFeedID: String) -> WebFeedMetadata {
|
||||
if let d = webFeedMetadata[feedURL] {
|
||||
assert(d.delegate === self)
|
||||
return d
|
||||
}
|
||||
let d = FeedMetadata(feedID: feedID)
|
||||
let d = WebFeedMetadata(webFeedID: webFeedID)
|
||||
d.delegate = self
|
||||
feedMetadata[feedURL] = d
|
||||
webFeedMetadata[feedURL] = d
|
||||
return d
|
||||
}
|
||||
|
||||
func updateFlattenedFeeds() {
|
||||
var feeds = Set<Feed>()
|
||||
feeds.formUnion(topLevelFeeds)
|
||||
func updateFlattenedWebFeeds() {
|
||||
var feeds = Set<WebFeed>()
|
||||
feeds.formUnion(topLevelWebFeeds)
|
||||
for folder in folders! {
|
||||
feeds.formUnion(folder.flattenedFeeds())
|
||||
feeds.formUnion(folder.flattenedWebFeeds())
|
||||
}
|
||||
|
||||
_flattenedFeeds = feeds
|
||||
flattenedFeedsNeedUpdate = false
|
||||
_flattenedWebFeeds = feeds
|
||||
flattenedWebFeedsNeedUpdate = false
|
||||
}
|
||||
|
||||
func rebuildFeedDictionaries() {
|
||||
var idDictionary = [String: Feed]()
|
||||
func rebuildWebFeedDictionaries() {
|
||||
var idDictionary = [String: WebFeed]()
|
||||
|
||||
flattenedFeeds().forEach { (feed) in
|
||||
idDictionary[feed.feedID] = feed
|
||||
flattenedWebFeeds().forEach { (feed) in
|
||||
idDictionary[feed.webFeedID] = feed
|
||||
}
|
||||
|
||||
_idToFeedDictionary = idDictionary
|
||||
feedDictionaryNeedsUpdate = false
|
||||
_idToWebFeedDictionary = idDictionary
|
||||
webFeedDictionaryNeedsUpdate = false
|
||||
}
|
||||
|
||||
func updateUnreadCount() {
|
||||
|
@ -998,21 +1038,21 @@ private extension Account {
|
|||
return
|
||||
}
|
||||
var updatedUnreadCount = 0
|
||||
for feed in flattenedFeeds() {
|
||||
for feed in flattenedWebFeeds() {
|
||||
updatedUnreadCount += feed.unreadCount
|
||||
}
|
||||
unreadCount = updatedUnreadCount
|
||||
}
|
||||
|
||||
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
||||
let feeds = Set(articles.compactMap { $0.feed })
|
||||
let feeds = Set(articles.compactMap { $0.webFeed })
|
||||
let statuses = Set(articles.map { $0.status })
|
||||
|
||||
// .UnreadCountDidChange notification will get sent to Folder and Account objects,
|
||||
// which will update their own unread counts.
|
||||
updateUnreadCounts(for: feeds)
|
||||
|
||||
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.feeds: feeds])
|
||||
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.webFeeds: feeds])
|
||||
}
|
||||
|
||||
func fetchAllUnreadCounts() {
|
||||
|
@ -1026,10 +1066,10 @@ private extension Account {
|
|||
return
|
||||
}
|
||||
|
||||
self.flattenedFeeds().forEach{ (feed) in
|
||||
self.flattenedWebFeeds().forEach{ (feed) in
|
||||
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
||||
|
||||
if let unreadCount = unreadCountDictionary[feed.feedID] {
|
||||
if let unreadCount = unreadCountDictionary[feed.webFeedID] {
|
||||
feed.unreadCount = unreadCount
|
||||
}
|
||||
else {
|
||||
|
@ -1047,8 +1087,8 @@ private extension Account {
|
|||
|
||||
extension Account {
|
||||
|
||||
public func existingFeed(withFeedID feedID: String) -> Feed? {
|
||||
return idToFeedDictionary[feedID]
|
||||
public func existingWebFeed(withWebFeedID webFeedID: String) -> WebFeed? {
|
||||
return idToWebFeedDictionary[webFeedID]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1058,7 +1098,7 @@ extension Account: OPMLRepresentable {
|
|||
|
||||
public func OPMLString(indentLevel: Int, strictConformance: Bool) -> String {
|
||||
var s = ""
|
||||
for feed in topLevelFeeds {
|
||||
for feed in topLevelWebFeeds {
|
||||
s += feed.OPMLString(indentLevel: indentLevel + 1, strictConformance: strictConformance)
|
||||
}
|
||||
for folder in folders! {
|
||||
|
|
|
@ -20,12 +20,11 @@
|
|||
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
|
||||
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
|
||||
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
|
||||
510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */; };
|
||||
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */; };
|
||||
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9803237CD4270028BCAA /* FeedIdentifier.swift */; };
|
||||
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; };
|
||||
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; };
|
||||
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; };
|
||||
5133230E2281089500C30F19 /* icons.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230D2281089500C30F19 /* icons.json */; };
|
||||
5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133230F22810E5700C30F19 /* FeedbinIcon.swift */; };
|
||||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; };
|
||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; };
|
||||
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */; };
|
||||
|
@ -42,6 +41,7 @@
|
|||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; };
|
||||
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
|
||||
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
|
||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
|
||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
|
||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
|
||||
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
|
||||
|
@ -52,7 +52,6 @@
|
|||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; };
|
||||
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; };
|
||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; };
|
||||
51FE1008234635A20056195D /* DeepLinkProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE1007234635A20056195D /* DeepLinkProvider.swift */; };
|
||||
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; };
|
||||
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; };
|
||||
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; };
|
||||
|
@ -67,7 +66,7 @@
|
|||
841D4D702106B40400DD04E6 /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D6F2106B40400DD04E6 /* ArticlesDatabase.framework */; };
|
||||
841D4D722106B40A00DD04E6 /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841D4D712106B40A00DD04E6 /* Articles.framework */; };
|
||||
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */; };
|
||||
844B297D2106C7EC004020B3 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297C2106C7EC004020B3 /* Feed.swift */; };
|
||||
844B297D2106C7EC004020B3 /* WebFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297C2106C7EC004020B3 /* WebFeed.swift */; };
|
||||
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */; };
|
||||
844B2981210CE3BF004020B3 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844B2980210CE3BF004020B3 /* RSWeb.framework */; };
|
||||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848935101F62486800CEBD24 /* Account.swift */; };
|
||||
|
@ -76,7 +75,7 @@
|
|||
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; };
|
||||
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846E77531F6F00E300A165E2 /* AccountManager.swift */; };
|
||||
848935001F62484F00CEBD24 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848934F61F62484F00CEBD24 /* Account.framework */; };
|
||||
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */; };
|
||||
84B2D4D02238CD8A00498ADA /* WebFeedMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B2D4CE2238C13D00498ADA /* WebFeedMetadata.swift */; };
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */; };
|
||||
84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */; };
|
||||
84C8B3F41F89DE430053CCA6 /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */; };
|
||||
|
@ -85,6 +84,11 @@
|
|||
84EAC4822148CC6300F154AB /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84EAC4812148CC6300F154AB /* RSDatabase.framework */; };
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */; };
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */; };
|
||||
9E0260CB236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0260CA236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift */; };
|
||||
9E03C11C235D921400FB6D9E /* FeedlyOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */; };
|
||||
9E03C11E235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */; };
|
||||
9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C11F235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift */; };
|
||||
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C121235E62E100FB6D9E /* FeedlyTestSupport.swift */; };
|
||||
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */; };
|
||||
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D22345700E0056A5A8 /* FeedlyLink.swift */; };
|
||||
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */; };
|
||||
|
@ -92,29 +96,44 @@
|
|||
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */; };
|
||||
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */; };
|
||||
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */; };
|
||||
9E1D154D233370D800F4944C /* FeedlySyncStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */; };
|
||||
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */; };
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; };
|
||||
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; };
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15522334304B00F4944C /* FeedlyGetStreamOperation.swift */; };
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15522334304B00F4944C /* FeedlyGetStreamContentsOperation.swift */; };
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D1554233431A600F4944C /* FeedlyOperation.swift */; };
|
||||
9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */; };
|
||||
9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */; };
|
||||
9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */; };
|
||||
9E1FF8602368216B00834C24 /* TestGetStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF85F2368216B00834C24 /* TestGetStreamIdsService.swift */; };
|
||||
9E1FF8622368219B00834C24 /* TestGetPagedStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */; };
|
||||
9E1FF8642368EC2400834C24 /* FeedlySyncAllOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */; };
|
||||
9E1FF8662368ED7E00834C24 /* TestMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */; };
|
||||
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */; };
|
||||
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */; };
|
||||
9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */; };
|
||||
9E489E8D2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */; };
|
||||
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */; };
|
||||
9E489E93236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */; };
|
||||
9E510D6E234F16A8002E6F1A /* FeedlyAddFeedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */; };
|
||||
9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */; };
|
||||
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */ = {isa = PBXBuildFile; fileRef = 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */; };
|
||||
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */; };
|
||||
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */; };
|
||||
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */; };
|
||||
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */; };
|
||||
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */; };
|
||||
9E7F15072341E96700F860D1 /* AccountFeedlySyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */; };
|
||||
9E7F150A2341EF5A00F860D1 /* feedly_collections_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */; };
|
||||
9E7F150D2341F32000F860D1 /* macintosh_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F150C2341F32000F860D1 /* macintosh_initial.json */; };
|
||||
9E7F15112341F39A00F860D1 /* uncategorized_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15102341F39A00F860D1 /* uncategorized_initial.json */; };
|
||||
9E7F15132341F3D900F860D1 /* programming_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15122341F3D900F860D1 /* programming_initial.json */; };
|
||||
9E7F15152341F42000F860D1 /* weblogs_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15142341F42000F860D1 /* weblogs_initial.json */; };
|
||||
9E7F15172341F48900F860D1 /* mustread_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15162341F48900F860D1 /* mustread_initial.json */; };
|
||||
9E832B1E2343467900D83249 /* feedly_collections_addcollection.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B1D2343467900D83249 /* feedly_collections_addcollection.json */; };
|
||||
9E832B202343476A00D83249 /* newcollection_addcollection.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B1F2343476A00D83249 /* newcollection_addcollection.json */; };
|
||||
9E832B23234416B400D83249 /* feedly_collections_addfeed.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B22234416B400D83249 /* feedly_collections_addfeed.json */; };
|
||||
9E832B25234416FF00D83249 /* mustread_addfeed.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B24234416FF00D83249 /* mustread_addfeed.json */; };
|
||||
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */; };
|
||||
9E784EC0237E8BE100099B1B /* FeedlyLogoutOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */; };
|
||||
9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */; };
|
||||
9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */; };
|
||||
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */; };
|
||||
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */; };
|
||||
9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */; };
|
||||
9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */; };
|
||||
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; };
|
||||
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; };
|
||||
9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */; };
|
||||
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */; };
|
||||
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */; };
|
||||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; };
|
||||
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; };
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; };
|
||||
|
@ -122,14 +141,33 @@
|
|||
9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC625233318400085D7C9 /* FeedlyStream.swift */; };
|
||||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; };
|
||||
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */; };
|
||||
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */; };
|
||||
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */; };
|
||||
9EC228592362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */; };
|
||||
9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */; };
|
||||
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 */; };
|
||||
9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */; };
|
||||
9EC804E5236C1A7F0057CFCB /* feedly-2-changestatuses in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */; };
|
||||
9EC804E7236C1BA60057CFCB /* feedly-3-changestatusesagain in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */; };
|
||||
9EC804E9236C1CBF0057CFCB /* feedly-4-addfeedsandfolders in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804E8236C1CBE0057CFCB /* feedly-4-addfeedsandfolders */; };
|
||||
9EC804EB236C1DFB0057CFCB /* feedly-5-removefeedsandfolders in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804EA236C1DFA0057CFCB /* feedly-5-removefeedsandfolders */; };
|
||||
9EC804ED236C206A0057CFCB /* feedly_collections_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804EC236C206A0057CFCB /* feedly_collections_initial.json */; };
|
||||
9EC804EF236C20DD0057CFCB /* feedly_macintosh_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804EE236C20DD0057CFCB /* feedly_macintosh_initial.json */; };
|
||||
9EC804F2236C21320057CFCB /* feedly_unreads_1000.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804F0236C21320057CFCB /* feedly_unreads_1000.json */; };
|
||||
9EC804F3236C21320057CFCB /* feedly_unreads_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804F1236C21320057CFCB /* feedly_unreads_initial.json */; };
|
||||
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */; };
|
||||
9EE4CCFA234F106600FBAE4B /* FeedlyFeedContainerValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */; };
|
||||
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE06D235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift */; };
|
||||
9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE070235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift */; };
|
||||
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */; };
|
||||
9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */; };
|
||||
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */; };
|
||||
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */; };
|
||||
9EEEF75223567CA6009E9D80 /* saved_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EEEF75123567CA6009E9D80 /* saved_initial.json */; };
|
||||
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */; };
|
||||
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */; };
|
||||
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */; };
|
||||
9EF35F7A234E830E003AE2AE /* FeedlyCompoundOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -192,12 +230,11 @@
|
|||
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
|
||||
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
|
||||
510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; };
|
||||
510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMetadataFile.swift; sourceTree = "<group>"; };
|
||||
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedMetadataFile.swift; sourceTree = "<group>"; };
|
||||
511B9803237CD4270028BCAA /* FeedIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIdentifier.swift; sourceTree = "<group>"; };
|
||||
513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = "<group>"; };
|
||||
513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; };
|
||||
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
|
||||
5133230D2281089500C30F19 /* icons.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = icons.json; sourceTree = "<group>"; };
|
||||
5133230F22810E5700C30F19 /* FeedbinIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinIcon.swift; sourceTree = "<group>"; };
|
||||
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = "<group>"; };
|
||||
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = "<group>"; };
|
||||
|
@ -215,6 +252,7 @@
|
|||
5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = "<group>"; };
|
||||
518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = "<group>"; };
|
||||
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
|
||||
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
||||
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
|
||||
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
|
||||
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
|
||||
|
@ -225,7 +263,6 @@
|
|||
51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = "<group>"; };
|
||||
51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = "<group>"; };
|
||||
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = "<group>"; };
|
||||
51FE1007234635A20056195D /* DeepLinkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkProvider.swift; sourceTree = "<group>"; };
|
||||
552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = "<group>"; };
|
||||
552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = "<group>"; };
|
||||
552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = "<group>"; };
|
||||
|
@ -243,7 +280,7 @@
|
|||
841D4D6F2106B40400DD04E6 /* ArticlesDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ArticlesDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
841D4D712106B40A00DD04E6 /* Articles.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Articles.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinSubscription.swift; sourceTree = "<group>"; };
|
||||
844B297C2106C7EC004020B3 /* Feed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
||||
844B297C2106C7EC004020B3 /* WebFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebFeed.swift; sourceTree = "<group>"; };
|
||||
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProvider.swift; sourceTree = "<group>"; };
|
||||
844B2980210CE3BF004020B3 /* RSWeb.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSWeb.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
846E77531F6F00E300A165E2 /* AccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -253,7 +290,7 @@
|
|||
848935061F62485000CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
848935101F62486800CEBD24 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadata.swift; sourceTree = "<group>"; };
|
||||
84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMetadata.swift; sourceTree = "<group>"; };
|
||||
84B2D4CE2238C13D00498ADA /* WebFeedMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedMetadata.swift; sourceTree = "<group>"; };
|
||||
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerPath.swift; sourceTree = "<group>"; };
|
||||
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedRefreshProgress.swift; sourceTree = "<group>"; };
|
||||
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = "<group>"; };
|
||||
|
@ -261,6 +298,11 @@
|
|||
84D09622217418DC00D77525 /* FeedbinTagging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTagging.swift; sourceTree = "<group>"; };
|
||||
84EAC4812148CC6300F154AB /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleFetcher.swift; sourceTree = "<group>"; };
|
||||
9E0260CA236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshAccessTokenOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C11F235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C121235E62E100FB6D9E /* FeedlyTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTestSupport.swift; sourceTree = "<group>"; };
|
||||
9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperation.swift; sourceTree = "<group>"; };
|
||||
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyLink.swift; sourceTree = "<group>"; };
|
||||
9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntryParser.swift; sourceTree = "<group>"; };
|
||||
|
@ -268,29 +310,44 @@
|
|||
9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceId.swift; sourceTree = "<group>"; };
|
||||
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceIdTests.swift; sourceTree = "<group>"; };
|
||||
9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStrategy.swift; sourceTree = "<group>"; };
|
||||
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamContentsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRequestStreamsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1FF85F2368216B00834C24 /* TestGetStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetStreamIdsService.swift; sourceTree = "<group>"; };
|
||||
9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamIdsService.swift; sourceTree = "<group>"; };
|
||||
9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMarkArticlesService.swift; sourceTree = "<group>"; };
|
||||
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetCollectionsService.swift; sourceTree = "<group>"; };
|
||||
9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedRequest.swift; sourceTree = "<group>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshStreamEntriesStatusOperation.swift; sourceTree = "<group>"; };
|
||||
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-1-initial"; sourceTree = "<group>"; };
|
||||
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshAccessTokenOperation.swift; sourceTree = "<group>"; };
|
||||
9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAcessTokenRefreshing.swift; sourceTree = "<group>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceProviding.swift; sourceTree = "<group>"; };
|
||||
9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedlySyncTest.swift; sourceTree = "<group>"; };
|
||||
9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_initial.json; sourceTree = "<group>"; };
|
||||
9E7F150C2341F32000F860D1 /* macintosh_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = macintosh_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15102341F39A00F860D1 /* uncategorized_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = uncategorized_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15122341F3D900F860D1 /* programming_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = programming_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15142341F42000F860D1 /* weblogs_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weblogs_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15162341F48900F860D1 /* mustread_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mustread_initial.json; sourceTree = "<group>"; };
|
||||
9E832B1D2343467900D83249 /* feedly_collections_addcollection.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_addcollection.json; sourceTree = "<group>"; };
|
||||
9E832B1F2343476A00D83249 /* newcollection_addcollection.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = newcollection_addcollection.json; sourceTree = "<group>"; };
|
||||
9E832B22234416B400D83249 /* feedly_collections_addfeed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_addfeed.json; sourceTree = "<group>"; };
|
||||
9E832B24234416FF00D83249 /* mustread_addfeed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mustread_addfeed.json; sourceTree = "<group>"; };
|
||||
9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperation.swift; sourceTree = "<group>"; };
|
||||
9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperation.swift; sourceTree = "<group>"; };
|
||||
9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperation.swift; sourceTree = "<group>"; };
|
||||
9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetStreamContentsService.swift; sourceTree = "<group>"; };
|
||||
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = "<group>"; };
|
||||
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = "<group>"; };
|
||||
9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesService.swift; sourceTree = "<group>"; };
|
||||
9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OAuthAuthorizationClient+Feedly.swift"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -298,14 +355,33 @@
|
|||
9EAEC625233318400085D7C9 /* FeedlyStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStream.swift; sourceTree = "<group>"; };
|
||||
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCategory.swift; sourceTree = "<group>"; };
|
||||
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrigin.swift; sourceTree = "<group>"; };
|
||||
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperationTests.swift; sourceTree = "<group>"; };
|
||||
9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperationTests.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllMockResponseProvider.swift; sourceTree = "<group>"; };
|
||||
9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-2-changestatuses"; sourceTree = "<group>"; };
|
||||
9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-3-changestatusesagain"; sourceTree = "<group>"; };
|
||||
9EC804E8236C1CBE0057CFCB /* feedly-4-addfeedsandfolders */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-4-addfeedsandfolders"; sourceTree = "<group>"; };
|
||||
9EC804EA236C1DFA0057CFCB /* feedly-5-removefeedsandfolders */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-5-removefeedsandfolders"; sourceTree = "<group>"; };
|
||||
9EC804EC236C206A0057CFCB /* feedly_collections_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_initial.json; sourceTree = "<group>"; };
|
||||
9EC804EE236C20DD0057CFCB /* feedly_macintosh_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_macintosh_initial.json; sourceTree = "<group>"; };
|
||||
9EC804F0236C21320057CFCB /* feedly_unreads_1000.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_unreads_1000.json; sourceTree = "<group>"; };
|
||||
9EC804F1236C21320057CFCB /* feedly_unreads_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_unreads_initial.json; sourceTree = "<group>"; };
|
||||
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegateError.swift; sourceTree = "<group>"; };
|
||||
9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedContainerValidator.swift; sourceTree = "<group>"; };
|
||||
9EEAE06D235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsService.swift; sourceTree = "<group>"; };
|
||||
9EEAE070235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsService.swift; sourceTree = "<group>"; };
|
||||
9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsService.swift; sourceTree = "<group>"; };
|
||||
9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMarkArticlesService.swift; sourceTree = "<group>"; };
|
||||
9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperation.swift; sourceTree = "<group>"; };
|
||||
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9EEEF75123567CA6009E9D80 /* saved_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = saved_initial.json; sourceTree = "<group>"; };
|
||||
9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperation.swift; sourceTree = "<group>"; };
|
||||
9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperation.swift; sourceTree = "<group>"; };
|
||||
9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStreamIds.swift; sourceTree = "<group>"; };
|
||||
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCompoundOperation.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>"; };
|
||||
|
@ -389,7 +465,10 @@
|
|||
51D58756227F62E300900287 /* JSON */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5133230D2281089500C30F19 /* icons.json */,
|
||||
9EC804F0236C21320057CFCB /* feedly_unreads_1000.json */,
|
||||
9EC804F1236C21320057CFCB /* feedly_unreads_initial.json */,
|
||||
9EC804EE236C20DD0057CFCB /* feedly_macintosh_initial.json */,
|
||||
9EC804EC236C206A0057CFCB /* feedly_collections_initial.json */,
|
||||
5133230B2281088A00C30F19 /* subscriptions_add.json */,
|
||||
513323092281082F00C30F19 /* subscriptions_initial.json */,
|
||||
5165D71422821C2400D9D53D /* taggings_add.json */,
|
||||
|
@ -452,7 +531,6 @@
|
|||
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */,
|
||||
51E490352288C37100C791F0 /* FeedbinDate.swift */,
|
||||
84CAD7151FDF2E22000F0755 /* FeedbinEntry.swift */,
|
||||
5133230F22810E5700C30F19 /* FeedbinIcon.swift */,
|
||||
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */,
|
||||
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */,
|
||||
84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */,
|
||||
|
@ -493,12 +571,13 @@
|
|||
8419740D1F6DD25F006346C4 /* Container.swift */,
|
||||
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
|
||||
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */,
|
||||
844B297C2106C7EC004020B3 /* Feed.swift */,
|
||||
84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */,
|
||||
510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */,
|
||||
51BC8FCB237EC055004F8B56 /* Feed.swift */,
|
||||
511B9803237CD4270028BCAA /* FeedIdentifier.swift */,
|
||||
841974001F6DD1EC006346C4 /* Folder.swift */,
|
||||
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
|
||||
51FE1007234635A20056195D /* DeepLinkProvider.swift */,
|
||||
844B297C2106C7EC004020B3 /* WebFeed.swift */,
|
||||
84B2D4CE2238C13D00498ADA /* WebFeedMetadata.swift */,
|
||||
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */,
|
||||
5165D71F22835E9800D9D53D /* FeedFinder */,
|
||||
515E4EB12324FF7D0057B0E7 /* Credentials */,
|
||||
8419742B1F6DDE84006346C4 /* LocalAccount */,
|
||||
|
@ -541,47 +620,42 @@
|
|||
9E7F15082341E97100F860D1 /* Feedly */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */,
|
||||
9E03C121235E62E100FB6D9E /* FeedlyTestSupport.swift */,
|
||||
9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */,
|
||||
9E1FF85F2368216B00834C24 /* TestGetStreamIdsService.swift */,
|
||||
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */,
|
||||
9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */,
|
||||
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */,
|
||||
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */,
|
||||
9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */,
|
||||
9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */,
|
||||
9E03C11F235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift */,
|
||||
9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */,
|
||||
9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */,
|
||||
9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */,
|
||||
9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */,
|
||||
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */,
|
||||
9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */,
|
||||
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */,
|
||||
9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */,
|
||||
9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */,
|
||||
9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */,
|
||||
9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */,
|
||||
9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */,
|
||||
9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */,
|
||||
9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */,
|
||||
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */,
|
||||
9E7F150B2341F2A700F860D1 /* Initial */,
|
||||
9E832B1A234344DA00D83249 /* AddCollection */,
|
||||
9E832B21234416B400D83249 /* AddFeed */,
|
||||
9E0260CA236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift */,
|
||||
9E784EBF237E8BE100099B1B /* FeedlyLogoutOperationTests.swift */,
|
||||
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */,
|
||||
9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */,
|
||||
9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */,
|
||||
9EC804E8236C1CBE0057CFCB /* feedly-4-addfeedsandfolders */,
|
||||
9EC804EA236C1DFA0057CFCB /* feedly-5-removefeedsandfolders */,
|
||||
);
|
||||
path = Feedly;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E7F150B2341F2A700F860D1 /* Initial */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EEEF75123567CA6009E9D80 /* saved_initial.json */,
|
||||
9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */,
|
||||
9E7F150C2341F32000F860D1 /* macintosh_initial.json */,
|
||||
9E7F15162341F48900F860D1 /* mustread_initial.json */,
|
||||
9E7F15122341F3D900F860D1 /* programming_initial.json */,
|
||||
9E7F15102341F39A00F860D1 /* uncategorized_initial.json */,
|
||||
9E7F15142341F42000F860D1 /* weblogs_initial.json */,
|
||||
);
|
||||
path = Initial;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E832B1A234344DA00D83249 /* AddCollection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E832B1D2343467900D83249 /* feedly_collections_addcollection.json */,
|
||||
9E832B1F2343476A00D83249 /* newcollection_addcollection.json */,
|
||||
);
|
||||
path = AddCollection;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E832B21234416B400D83249 /* AddFeed */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E832B22234416B400D83249 /* feedly_collections_addfeed.json */,
|
||||
9E832B24234416FF00D83249 /* mustread_addfeed.json */,
|
||||
);
|
||||
path = AddFeed;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EA31339231E368100268BA0 /* Feedly */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -589,36 +663,47 @@
|
|||
9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */,
|
||||
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */,
|
||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */,
|
||||
9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */,
|
||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
|
||||
9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */,
|
||||
9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */,
|
||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */,
|
||||
9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */,
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */,
|
||||
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */,
|
||||
9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */,
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */,
|
||||
9EEAE06F235D003400E3FEE4 /* Services */,
|
||||
9EBC31B32338AC2E002A567B /* Models */,
|
||||
9EBC31B22338AC0F002A567B /* Refresh */,
|
||||
9EBC31B22338AC0F002A567B /* Operations */,
|
||||
);
|
||||
path = Feedly;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
9EBC31B22338AC0F002A567B /* Refresh */ = {
|
||||
9EBC31B22338AC0F002A567B /* Operations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */,
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */,
|
||||
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */,
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */,
|
||||
9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */,
|
||||
9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */,
|
||||
9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */,
|
||||
9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */,
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamOperation.swift */,
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamContentsOperation.swift */,
|
||||
9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */,
|
||||
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */,
|
||||
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */,
|
||||
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */,
|
||||
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */,
|
||||
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */,
|
||||
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */,
|
||||
9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */,
|
||||
9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */,
|
||||
9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */,
|
||||
9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */,
|
||||
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */,
|
||||
9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */,
|
||||
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */,
|
||||
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */,
|
||||
9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */,
|
||||
);
|
||||
path = Refresh;
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EBC31B32338AC2E002A567B /* Models */ = {
|
||||
|
@ -630,6 +715,7 @@
|
|||
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */,
|
||||
9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */,
|
||||
9EAEC625233318400085D7C9 /* FeedlyStream.swift */,
|
||||
9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */,
|
||||
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */,
|
||||
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */,
|
||||
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */,
|
||||
|
@ -638,6 +724,18 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EEAE06F235D003400E3FEE4 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EEAE06D235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift */,
|
||||
9EEAE070235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift */,
|
||||
9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */,
|
||||
9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */,
|
||||
9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D511EEB4202422BB00712EC3 /* xcconfig */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -668,9 +766,15 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 8489350A1F62485000CEBD24 /* Build configuration list for PBXNativeTarget "Account" */;
|
||||
buildPhases = (
|
||||
<<<<<<< HEAD
|
||||
3BF610B123571C31000EF978 /* Run Script: Update FeedWranglerConfig.swift */,
|
||||
848934F11F62484F00CEBD24 /* Sources */,
|
||||
3BF610B223571C32000EF978 /* Run Script: Reset FeedWranglerConfig.swift */,
|
||||
=======
|
||||
9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */,
|
||||
848934F11F62484F00CEBD24 /* Sources */,
|
||||
9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */,
|
||||
>>>>>>> master
|
||||
848934F21F62484F00CEBD24 /* Frameworks */,
|
||||
848934F31F62484F00CEBD24 /* Headers */,
|
||||
848934F41F62484F00CEBD24 /* Resources */,
|
||||
|
@ -716,11 +820,11 @@
|
|||
848934F51F62484F00CEBD24 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 0900;
|
||||
ProvisioningStyle = Manual;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
848934FE1F62484F00CEBD24 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
ProvisioningStyle = Manual;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -802,25 +906,22 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9EC804ED236C206A0057CFCB /* feedly_collections_initial.json in Resources */,
|
||||
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */,
|
||||
5165D71822821C2400D9D53D /* taggings_initial.json in Resources */,
|
||||
5133230E2281089500C30F19 /* icons.json in Resources */,
|
||||
9EC804F3236C21320057CFCB /* feedly_unreads_initial.json in Resources */,
|
||||
51D5875B227F630B00900287 /* tags_add.json in Resources */,
|
||||
9EC804E9236C1CBF0057CFCB /* feedly-4-addfeedsandfolders in Resources */,
|
||||
9EC804EB236C1DFB0057CFCB /* feedly-5-removefeedsandfolders in Resources */,
|
||||
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */,
|
||||
9E832B202343476A00D83249 /* newcollection_addcollection.json in Resources */,
|
||||
9E832B1E2343467900D83249 /* feedly_collections_addcollection.json in Resources */,
|
||||
9E7F15152341F42000F860D1 /* weblogs_initial.json in Resources */,
|
||||
51D5875C227F630B00900287 /* tags_initial.json in Resources */,
|
||||
9EC804E5236C1A7F0057CFCB /* feedly-2-changestatuses in Resources */,
|
||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */,
|
||||
9EC804E7236C1BA60057CFCB /* feedly-3-changestatusesagain in Resources */,
|
||||
9EC804EF236C20DD0057CFCB /* feedly_macintosh_initial.json in Resources */,
|
||||
5165D71722821C2400D9D53D /* taggings_add.json in Resources */,
|
||||
9EEEF75223567CA6009E9D80 /* saved_initial.json in Resources */,
|
||||
9E832B23234416B400D83249 /* feedly_collections_addfeed.json in Resources */,
|
||||
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */,
|
||||
9E7F15112341F39A00F860D1 /* uncategorized_initial.json in Resources */,
|
||||
9E7F15132341F3D900F860D1 /* programming_initial.json in Resources */,
|
||||
9E832B25234416FF00D83249 /* mustread_addfeed.json in Resources */,
|
||||
9E7F150A2341EF5A00F860D1 /* feedly_collections_initial.json in Resources */,
|
||||
9E7F15172341F48900F860D1 /* mustread_initial.json in Resources */,
|
||||
9E7F150D2341F32000F860D1 /* macintosh_initial.json in Resources */,
|
||||
9EC804F2236C21320057CFCB /* feedly_unreads_1000.json in Resources */,
|
||||
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -882,6 +983,42 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
|
||||
};
|
||||
9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script: Update OAuthAuthorizationClient+Feedly.swift";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "FAILED=false\n\nif [ -z \"${FEEDLY_CLIENT_ID}\" ]; then\necho \"Missing Feedly Client ID\"\nFAILED=true\nfi\n\nif [ -z \"${FEEDLY_CLIENT_SECRET}\" ]; then\necho \"Missing Feedly Client Secret\"\nFAILED=true\nfi\n\nFEEDLY_CLIENT_SOURCE=\"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feedly client ID or secret. ${FEEDLY_CLIENT_SOURCE} not changed.\"\nexit 0\nfi\n\n# echo \"Substituting variables in: ${FEEDLY_CLIENT_SOURCE}\"\n\nif [ -e \"${FEEDLY_CLIENT_SOURCE}\" ]\nthen\n sed -i .tmp \"s|{FEEDLY_CLIENT_ID}|${FEEDLY_CLIENT_ID}|g; s|{FEEDLY_CLIENT_SECRET}|${FEEDLY_CLIENT_SECRET}|g\" $FEEDLY_CLIENT_SOURCE\n # echo \"`git diff ${FEEDLY_CLIENT_SOURCE}`\"\n rm -f \"${FEEDLY_CLIENT_SOURCE}.tmp\"\nelse\n echo \"File does not exist at ${FEEDLY_CLIENT_SOURCE}. Has it been moved or renamed?\"\n exit -1\nfi\n\necho \"All env values found!\"\n";
|
||||
};
|
||||
9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script: Reset OAuthAuthorizationClient+Feedly.swift";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "git checkout \"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
@ -894,7 +1031,9 @@
|
|||
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */,
|
||||
84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */,
|
||||
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */,
|
||||
9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */,
|
||||
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */,
|
||||
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */,
|
||||
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */,
|
||||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
|
||||
|
@ -904,37 +1043,58 @@
|
|||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
||||
3BF6112423572A62000EF978 /* FeedWranglerSubscription.swift in Sources */,
|
||||
846E77451F6EF9B900A165E2 /* Container.swift in Sources */,
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamOperation.swift in Sources */,
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */,
|
||||
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,
|
||||
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
|
||||
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
|
||||
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */,
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
|
||||
<<<<<<< HEAD
|
||||
9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */,
|
||||
3BF610C923571CD4000EF978 /* FeedWranglerAccountDelegate.swift in Sources */,
|
||||
=======
|
||||
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */,
|
||||
>>>>>>> master
|
||||
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
|
||||
510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */,
|
||||
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
|
||||
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
|
||||
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
|
||||
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
|
||||
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */,
|
||||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
|
||||
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */,
|
||||
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
|
||||
<<<<<<< HEAD
|
||||
9E1D154D233370D800F4944C /* FeedlySyncStrategy.swift in Sources */,
|
||||
844B297D2106C7EC004020B3 /* Feed.swift in Sources */,
|
||||
3BF6119023577173000EF978 /* FeedWranglerGenericResult.swift in Sources */,
|
||||
=======
|
||||
9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */,
|
||||
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */,
|
||||
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
|
||||
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
|
||||
844B297D2106C7EC004020B3 /* WebFeed.swift in Sources */,
|
||||
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */,
|
||||
>>>>>>> master
|
||||
9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */,
|
||||
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */,
|
||||
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */,
|
||||
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */,
|
||||
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */,
|
||||
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedOperation.swift in Sources */,
|
||||
9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */,
|
||||
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */,
|
||||
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
|
||||
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */,
|
||||
84B2D4D02238CD8A00498ADA /* WebFeedMetadata.swift in Sources */,
|
||||
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */,
|
||||
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */,
|
||||
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */,
|
||||
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */,
|
||||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
9E510D6E234F16A8002E6F1A /* FeedlyAddFeedRequest.swift in Sources */,
|
||||
5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */,
|
||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */,
|
||||
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
|
||||
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
|
||||
|
@ -957,10 +1117,11 @@
|
|||
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */,
|
||||
9EE4CCFA234F106600FBAE4B /* FeedlyFeedContainerValidator.swift in Sources */,
|
||||
552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */,
|
||||
51FE1008234635A20056195D /* DeepLinkProvider.swift in Sources */,
|
||||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */,
|
||||
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */,
|
||||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,
|
||||
9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */,
|
||||
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
|
||||
|
@ -976,6 +1137,7 @@
|
|||
3BF610C823571CD4000EF978 /* FeedWranglerConfig.swift in Sources */,
|
||||
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */,
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */,
|
||||
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -984,14 +1146,39 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9E7F15072341E96700F860D1 /* AccountFeedlySyncTest.swift in Sources */,
|
||||
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */,
|
||||
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */,
|
||||
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */,
|
||||
9E784EC0237E8BE100099B1B /* FeedlyLogoutOperationTests.swift in Sources */,
|
||||
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */,
|
||||
9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */,
|
||||
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */,
|
||||
9E0260CB236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift in Sources */,
|
||||
9E1FF8622368219B00834C24 /* TestGetPagedStreamIdsService.swift in Sources */,
|
||||
9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */,
|
||||
9E03C11E235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift in Sources */,
|
||||
9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */,
|
||||
9E1FF8662368ED7E00834C24 /* TestMarkArticlesService.swift in Sources */,
|
||||
9E03C11C235D921400FB6D9E /* FeedlyOperationTests.swift in Sources */,
|
||||
9E1FF8642368EC2400834C24 /* FeedlySyncAllOperationTests.swift in Sources */,
|
||||
9E1FF8602368216B00834C24 /* TestGetStreamIdsService.swift in Sources */,
|
||||
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */,
|
||||
9EC228592362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift in Sources */,
|
||||
5165D7122282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift in Sources */,
|
||||
9E489E93236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift in Sources */,
|
||||
9E489E8D2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift in Sources */,
|
||||
51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */,
|
||||
9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */,
|
||||
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */,
|
||||
9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */,
|
||||
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
|
||||
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */,
|
||||
9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */,
|
||||
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
|
||||
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */,
|
||||
9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */,
|
||||
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */,
|
||||
9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -22,9 +22,10 @@ protocol AccountDelegate {
|
|||
|
||||
var refreshProgress: DownloadProgress { get }
|
||||
|
||||
func cancelAll(for account: Account)
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void))
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void))
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
|
@ -32,19 +33,21 @@ protocol AccountDelegate {
|
|||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void)
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void)
|
||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func addWebFeed(for account: Account, with: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>?
|
||||
|
||||
// Called at the end of account’s init method.
|
||||
func accountDidInitialize(_ account: Account)
|
||||
|
||||
func accountWillBeDeleted(_ account: Account)
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void)
|
||||
|
||||
|
|
|
@ -138,6 +138,8 @@ public final class AccountManager: UnreadCountProvider {
|
|||
return
|
||||
}
|
||||
|
||||
account.prepareForDeletion()
|
||||
|
||||
accountsDictionary.removeValue(forKey: account.accountID)
|
||||
account.isDeleted = true
|
||||
|
||||
|
@ -160,6 +162,10 @@ public final class AccountManager: UnreadCountProvider {
|
|||
return accountsDictionary[accountID]
|
||||
}
|
||||
|
||||
public func suspendAll() {
|
||||
accounts.forEach { $0.suspend() }
|
||||
}
|
||||
|
||||
public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
|
@ -187,7 +193,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||
|
||||
activeAccounts.forEach {
|
||||
group.enter()
|
||||
$0.syncArticleStatus() {
|
||||
$0.syncArticleStatus() { _ in
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
@ -203,7 +209,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||
|
||||
public func anyAccountHasAtLeastOneFeed() -> Bool {
|
||||
for account in activeAccounts {
|
||||
if account.hasAtLeastOneFeed() {
|
||||
if account.hasAtLeastOneWebFeed() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -213,7 +219,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||
|
||||
public func anyAccountHasFeedWithURL(_ urlString: String) -> Bool {
|
||||
for account in activeAccounts {
|
||||
if let _ = account.existingFeed(withURL: urlString) {
|
||||
if let _ = account.existingWebFeed(withURL: urlString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
|||
testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_add.json"
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/subscriptions.json"] = "subscriptions_initial.json"
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_initial.json"
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/icons.json"] = "icons.json"
|
||||
let account = TestAccountManager.shared.createAccount(type: .feedbin, transport: testTransport)
|
||||
|
||||
// Test initial folders
|
||||
|
@ -34,8 +33,8 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
|||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
let folder = account.folders?.filter { $0.name == "Developers" } .first!
|
||||
XCTAssertEqual(156, folder?.topLevelFeeds.count ?? 0)
|
||||
XCTAssertEqual(2, account.topLevelFeeds.count)
|
||||
XCTAssertEqual(156, folder?.topLevelWebFeeds.count ?? 0)
|
||||
XCTAssertEqual(2, account.topLevelWebFeeds.count)
|
||||
|
||||
// Test Adding a Feed to the folder
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_add.json"
|
||||
|
@ -46,8 +45,8 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
|||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(157, folder?.topLevelFeeds.count ?? 0)
|
||||
XCTAssertEqual(1, account.topLevelFeeds.count)
|
||||
XCTAssertEqual(157, folder?.topLevelWebFeeds.count ?? 0)
|
||||
XCTAssertEqual(1, account.topLevelWebFeeds.count)
|
||||
|
||||
// Test Deleting some Feeds from the folder
|
||||
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_delete.json"
|
||||
|
@ -58,8 +57,8 @@ class AccountFeedbinFolderContentsSyncTest: XCTestCase {
|
|||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(153, folder?.topLevelFeeds.count ?? 0)
|
||||
XCTAssertEqual(5, account.topLevelFeeds.count)
|
||||
XCTAssertEqual(153, folder?.topLevelWebFeeds.count ?? 0)
|
||||
XCTAssertEqual(5, account.topLevelWebFeeds.count)
|
||||
|
||||
TestAccountManager.shared.deleteAccount(account)
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ class AccountFeedbinSyncTest: XCTestCase {
|
|||
let testTransport = TestTransport()
|
||||
testTransport.testFiles["tags.json"] = "tags_add.json"
|
||||
testTransport.testFiles["subscriptions.json"] = "subscriptions_initial.json"
|
||||
testTransport.testFiles["icons.json"] = "icons.json"
|
||||
let account = TestAccountManager.shared.createAccount(type: .feedbin, transport: testTransport)
|
||||
|
||||
// Test initial folders
|
||||
|
@ -37,13 +36,12 @@ class AccountFeedbinSyncTest: XCTestCase {
|
|||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(224, account.flattenedFeeds().count)
|
||||
XCTAssertEqual(224, account.flattenedWebFeeds().count)
|
||||
|
||||
let daringFireball = account.idToFeedDictionary["1296379"]
|
||||
let daringFireball = account.idToWebFeedDictionary["1296379"]
|
||||
XCTAssertEqual("Daring Fireball", daringFireball!.name)
|
||||
XCTAssertEqual("https://daringfireball.net/feeds/json", daringFireball!.url)
|
||||
XCTAssertEqual("https://daringfireball.net/", daringFireball!.homePageURL)
|
||||
XCTAssertEqual("https://favicons.feedbinusercontent.com/6ac/6acc098f35ed2bcc0915ca89d50a97e5793eda45.png", daringFireball!.faviconURL)
|
||||
|
||||
// Test Adding a Feed
|
||||
testTransport.testFiles["subscriptions.json"] = "subscriptions_add.json"
|
||||
|
@ -59,13 +57,12 @@ class AccountFeedbinSyncTest: XCTestCase {
|
|||
}
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
|
||||
XCTAssertEqual(225, account.flattenedFeeds().count)
|
||||
XCTAssertEqual(225, account.flattenedWebFeeds().count)
|
||||
|
||||
let bPixels = account.idToFeedDictionary["1096623"]
|
||||
let bPixels = account.idToWebFeedDictionary["1096623"]
|
||||
XCTAssertEqual("Beautiful Pixels", bPixels?.name)
|
||||
XCTAssertEqual("https://feedpress.me/beautifulpixels", bPixels?.url)
|
||||
XCTAssertEqual("https://beautifulpixels.com/", bPixels?.homePageURL)
|
||||
XCTAssertEqual("https://favicons.feedbinusercontent.com/ea0/ea010c658d6e356e49ab239b793dc415af707b05.png", bPixels?.faviconURL)
|
||||
|
||||
TestAccountManager.shared.deleteAccount(account)
|
||||
|
||||
|
|
|
@ -1,324 +0,0 @@
|
|||
//
|
||||
// AccountFeedlySyncTest.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/9/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import Articles
|
||||
|
||||
class AccountFeedlySyncTest: XCTestCase {
|
||||
|
||||
private let testTransport = TestTransport()
|
||||
private var account: Account!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
account = TestAccountManager.shared.createAccount(type: .feedly, transport: testTransport)
|
||||
|
||||
do {
|
||||
let username = UUID().uuidString
|
||||
let credentials = Credentials(type: .oauthAccessToken, username: username, secret: "test")
|
||||
try account.storeCredentials(credentials)
|
||||
} catch {
|
||||
XCTFail("Unable to register mock credentials because \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Clean up
|
||||
do {
|
||||
try account.removeCredentials(type: .oauthAccessToken)
|
||||
} catch {
|
||||
XCTFail("Unable to clean up mock credentials because \(error)")
|
||||
}
|
||||
|
||||
TestAccountManager.shared.deleteAccount(account)
|
||||
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: Initial Sync
|
||||
|
||||
func testInitialSync() {
|
||||
XCTAssertTrue(account.idToFeedDictionary.isEmpty, "Expected to be testing a fresh account without any existing feeds.")
|
||||
XCTAssertTrue((account.folders ?? Set()).isEmpty, "Expected to be testing a fresh account without any existing folders.")
|
||||
|
||||
set(testFiles: .initial, with: testTransport)
|
||||
|
||||
// Test initial folders for collections and feeds for collection feeds.
|
||||
let initialExpection = self.expectation(description: "Initial feeds")
|
||||
account.refreshAll() { _ in
|
||||
initialExpection.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "macintosh_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "programming_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "uncategorized_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "weblogs_initial")
|
||||
}
|
||||
|
||||
// MARK: Add Collection
|
||||
|
||||
func testAddsFoldersForCollections() {
|
||||
prepareBaseline(.initial)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_initial")
|
||||
|
||||
set(testFiles: .addCollection, with: testTransport)
|
||||
|
||||
let addCollectionExpectation = self.expectation(description: "Adds NewCollection")
|
||||
account.refreshAll() { _ in
|
||||
addCollectionExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addcollection")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "newcollection_addcollection")
|
||||
}
|
||||
|
||||
// MARK: Add Feed
|
||||
|
||||
func testAddsFeeds() {
|
||||
prepareBaseline(.addCollection)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addcollection")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_initial")
|
||||
|
||||
set(testFiles: .addFeed, with: testTransport)
|
||||
|
||||
let addFeedExpectation = self.expectation(description: "Add Feed To Must Read (hey, that rhymes!)")
|
||||
account.refreshAll() { _ in
|
||||
addFeedExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addfeed")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_addfeed")
|
||||
}
|
||||
|
||||
// MARK: Remove Feed
|
||||
|
||||
func testRemovesFeeds() {
|
||||
prepareBaseline(.addFeed)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addfeed")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_addfeed")
|
||||
|
||||
set(testFiles: .removeFeed, with: testTransport)
|
||||
|
||||
let removeFeedExpectation = self.expectation(description: "Remove Feed from Must Read")
|
||||
account.refreshAll() { _ in
|
||||
removeFeedExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addcollection")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_initial")
|
||||
}
|
||||
|
||||
func testRemoveCollection() {
|
||||
prepareBaseline(.addFeed)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addfeed")
|
||||
|
||||
set(testFiles: .removeCollection, with: testTransport)
|
||||
|
||||
let removeCollectionExpectation = self.expectation(description: "Remove Collection")
|
||||
account.refreshAll() { _ in
|
||||
removeCollectionExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_initial")
|
||||
}
|
||||
|
||||
// MARK: Utility
|
||||
|
||||
func prepareBaseline(_ testFiles: TestFiles) {
|
||||
XCTAssertTrue(account.idToFeedDictionary.isEmpty, "Expected to be testing a fresh accout.")
|
||||
|
||||
set(testFiles: testFiles, with: testTransport)
|
||||
|
||||
// Test initial folders for collections and feeds for collection feeds.
|
||||
let preparationExpectation = self.expectation(description: "Prepare Account")
|
||||
account.refreshAll() { _ in
|
||||
preparationExpectation.fulfill()
|
||||
}
|
||||
// If there's a failure here, then an operation hasn't completed.
|
||||
// Check that test files have responses for all the requests this might make.
|
||||
waitForExpectations(timeout: 5)
|
||||
}
|
||||
|
||||
func checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed name: String) {
|
||||
let collections = testJSON(named: name) as! [[String:Any]]
|
||||
let collectionNames = Set(collections.map { $0["label"] as! String })
|
||||
let collectionIds = Set(collections.map { $0["id"] as! String })
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.name })
|
||||
let folderIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let missingNames = collectionNames.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderIds)
|
||||
|
||||
XCTAssertEqual(folders.count, collections.count, "Mismatch between collections and folders.")
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these names did not have a corresponding folder with the same name.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding folder with the same id.")
|
||||
|
||||
for collection in collections {
|
||||
checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONNamed name: String) {
|
||||
let collection = testJSON(named: name) as! [String:Any]
|
||||
checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONPayload collection: [String: Any]) {
|
||||
let label = collection["label"] as! String
|
||||
guard let folder = account.existingFolder(with: label) else {
|
||||
// due to a previous test failure?
|
||||
XCTFail("Could not find the \"\(label)\" folder.")
|
||||
return
|
||||
}
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let folderFeeds = folder.topLevelFeeds
|
||||
|
||||
XCTAssertEqual(collectionFeeds.count, folderFeeds.count)
|
||||
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
let folderFeedIds = Set(folderFeeds.map { $0.feedID })
|
||||
let missingFeedIds = collectionFeedIds.subtracting(folderFeedIds)
|
||||
|
||||
XCTAssertTrue(missingFeedIds.isEmpty, "Feeds with these ids were not found in the \"\(label)\" folder.")
|
||||
}
|
||||
|
||||
func checkArticles(againstItemsInStreamInJSONNamed name: String) {
|
||||
let stream = testJSON(named: name) as! [String:Any]
|
||||
checkArticles(againstItemsInStreamInJSONPayload: stream)
|
||||
}
|
||||
|
||||
func checkArticles(againstItemsInStreamInJSONPayload stream: [String: Any]) {
|
||||
|
||||
struct ArticleItem {
|
||||
var id: String
|
||||
var feedId: String
|
||||
var content: String
|
||||
var JSON: [String: Any]
|
||||
var unread: Bool
|
||||
|
||||
/// 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? {
|
||||
return ((JSON["canonical"] as? [[String: Any]]) ?? (JSON["alternate"] as? [[String: Any]]))?.compactMap { link -> String? in
|
||||
let href = link["href"] as? String
|
||||
if let type = link["type"] as? String {
|
||||
if type == "text/html" {
|
||||
return href
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return href
|
||||
}.first
|
||||
}
|
||||
|
||||
init(item: [String: Any]) {
|
||||
self.JSON = item
|
||||
self.id = item["id"] as! String
|
||||
|
||||
let origin = item["origin"] as! [String: Any]
|
||||
self.feedId = origin["streamId"] as! String
|
||||
|
||||
let content = item["content"] as? [String: Any]
|
||||
let summary = item["summary"] as? [String: Any]
|
||||
self.content = ((content ?? summary)?["content"] as? String) ?? ""
|
||||
|
||||
self.unread = item["unread"] as! Bool
|
||||
}
|
||||
}
|
||||
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let articleItems = items.map { ArticleItem(item: $0) }
|
||||
let itemIds = Set(articleItems.map { $0.id })
|
||||
|
||||
let articles = account.fetchArticles(.articleIDs(itemIds))
|
||||
let articleIds = Set(articles.map { $0.articleID })
|
||||
|
||||
let missing = itemIds.subtracting(articleIds)
|
||||
|
||||
XCTAssertEqual(items.count, articles.count)
|
||||
XCTAssertTrue(missing.isEmpty, "Items with these ids did not have a corresponding article with the same id.")
|
||||
|
||||
for article in articles {
|
||||
for item in articleItems where item.id == article.articleID {
|
||||
|
||||
XCTAssertEqual(article.uniqueID, item.id)
|
||||
XCTAssertEqual(article.contentHTML, item.content)
|
||||
XCTAssertEqual(article.feedID, item.feedId)
|
||||
XCTAssertEqual(article.externalURL, item.externalUrl)
|
||||
// XCTAssertEqual(article.status.boolStatus(forKey: .read), item.unread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testJSON(named: String) -> Any {
|
||||
let bundle = Bundle(for: TestTransport.self)
|
||||
let url = bundle.url(forResource: named, withExtension: "json")!
|
||||
let data = try! Data(contentsOf: url)
|
||||
let json = try! JSONSerialization.jsonObject(with: data)
|
||||
return json
|
||||
}
|
||||
|
||||
enum TestFiles {
|
||||
case initial
|
||||
case addCollection
|
||||
case addFeed
|
||||
case removeFeed
|
||||
case removeCollection
|
||||
}
|
||||
|
||||
func set(testFiles: TestFiles, with transport: TestTransport) {
|
||||
// TestTransport blacklists certain query items to make mocking responses easier.
|
||||
let collectionsEndpoint = "/v3/collections"
|
||||
switch testFiles {
|
||||
case .initial:
|
||||
let dict = [
|
||||
"/global.saved": "saved_initial.json",
|
||||
collectionsEndpoint: "feedly_collections_initial.json",
|
||||
"/5ca4d61d-e55d-4999-a8d1-c3b9d8789815": "macintosh_initial.json",
|
||||
"/global.must": "mustread_initial.json",
|
||||
"/885f2e01-d314-4e63-abac-17dcb063f5b5": "programming_initial.json",
|
||||
"/66132046-6f14-488d-b590-8e93422723c8": "uncategorized_initial.json",
|
||||
"/e31b3fcb-27f6-4f3e-b96c-53902586e366": "weblogs_initial.json",
|
||||
]
|
||||
transport.testFiles = dict
|
||||
|
||||
case .addCollection:
|
||||
set(testFiles: .initial, with: transport)
|
||||
|
||||
var dict = transport.testFiles
|
||||
dict[collectionsEndpoint] = "feedly_collections_addcollection.json"
|
||||
dict["/fc09f383-5a9a-4daa-a575-3efc1733b173"] = "newcollection_addcollection.json"
|
||||
transport.testFiles = dict
|
||||
|
||||
case .addFeed:
|
||||
set(testFiles: .addCollection, with: transport)
|
||||
|
||||
var dict = transport.testFiles
|
||||
dict[collectionsEndpoint] = "feedly_collections_addfeed.json"
|
||||
dict["/global.must"] = "mustread_addfeed.json"
|
||||
transport.testFiles = dict
|
||||
|
||||
case .removeFeed:
|
||||
set(testFiles: .addCollection, with: transport)
|
||||
|
||||
case .removeCollection:
|
||||
set(testFiles: .initial, with: transport)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// FeedlyCheckpointOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 25/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyCheckpointOperationTests: XCTestCase {
|
||||
|
||||
class TestDelegate: FeedlyCheckpointOperationDelegate {
|
||||
|
||||
var didReachCheckpointExpectation: XCTestExpectation?
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
didReachCheckpointExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testCallback() {
|
||||
let delegate = TestDelegate()
|
||||
delegate.didReachCheckpointExpectation = expectation(description: "Did Reach Checkpoint")
|
||||
|
||||
let operation = FeedlyCheckpointOperation()
|
||||
operation.checkpointDelegate = delegate
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
operation.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(operation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testCancellation() {
|
||||
let didReachCheckpointExpectation = expectation(description: "Did Reach Checkpoint")
|
||||
didReachCheckpointExpectation.isInverted = true
|
||||
|
||||
let delegate = TestDelegate()
|
||||
delegate.didReachCheckpointExpectation = didReachCheckpointExpectation
|
||||
|
||||
let operation = FeedlyCheckpointOperation()
|
||||
operation.checkpointDelegate = delegate
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
operation.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(operation)
|
||||
|
||||
operation.cancel()
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// FeedlyCreateFeedsForCollectionFoldersOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 22/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
class FeedsAndFoldersProvider: FeedlyFeedsAndFoldersProviding {
|
||||
var feedsAndFolders = [([FeedlyFeed], Folder)]()
|
||||
}
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
let folderOne: (name: String, id: String) = ("FolderOne", "folder/1")
|
||||
let folderTwo: (name: String, id: String) = ("FolderTwo", "folder/2")
|
||||
let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)]
|
||||
|
||||
let provider = FeedsAndFoldersProvider()
|
||||
provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in
|
||||
let accountFolder = account.ensureFolder(with: folder.name)!
|
||||
accountFolder.externalID = folder.id
|
||||
return (feeds, accountFolder)
|
||||
}
|
||||
|
||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
createFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(account.flattenedWebFeeds().isEmpty, "Expected empty account.")
|
||||
|
||||
OperationQueue.main.addOperation(createFeeds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feedIds = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.feedId })
|
||||
|
||||
let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.title })
|
||||
|
||||
let accountFeeds = account.flattenedWebFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.webFeedID })
|
||||
let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay })
|
||||
|
||||
let missingIds = feedIds.subtracting(ingestedIds)
|
||||
let missingTitles = feedTitles.subtracting(ingestedTitles)
|
||||
|
||||
XCTAssertTrue(missingIds.isEmpty, "Failed to ingest feeds with these ids.")
|
||||
XCTAssertTrue(missingTitles.isEmpty, "Failed to ingest feeds with these titles.")
|
||||
|
||||
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: <)]
|
||||
}
|
||||
|
||||
let ingestedFolderAndFeedIds = (account.folders ?? Set())
|
||||
.sorted { $0.externalID! < $1.externalID! }
|
||||
.compactMap { folder -> [String: [String]]? in
|
||||
return [folder.externalID!: folder.topLevelWebFeeds.map { $0.webFeedID }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds in their corresponding folders.")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
var feedsForFolderOne = [
|
||||
feedToRemove,
|
||||
FeedlyFeed(feedId: "feed/2", 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),
|
||||
]
|
||||
|
||||
// Add initial content.
|
||||
do {
|
||||
let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)]
|
||||
|
||||
let provider = FeedsAndFoldersProvider()
|
||||
provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in
|
||||
let accountFolder = account.ensureFolder(with: folder.name)!
|
||||
accountFolder.externalID = folder.id
|
||||
return (feeds, accountFolder)
|
||||
}
|
||||
|
||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
createFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(account.flattenedWebFeeds().isEmpty, "Expected empty account.")
|
||||
|
||||
OperationQueue.main.addOperation(createFeeds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
feedsForFolderOne.removeAll { $0.id == feedToRemove.id }
|
||||
feedsForFolderTwo.removeAll { $0.id == feedToRemove.id }
|
||||
let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)]
|
||||
|
||||
let provider = FeedsAndFoldersProvider()
|
||||
provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in
|
||||
let accountFolder = account.ensureFolder(with: folder.name)!
|
||||
accountFolder.externalID = folder.id
|
||||
return (feeds, accountFolder)
|
||||
}
|
||||
|
||||
let removeFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
removeFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(removeFeeds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feedIds = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.feedId })
|
||||
|
||||
let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.title })
|
||||
|
||||
let accountFeeds = account.flattenedWebFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.webFeedID })
|
||||
let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay })
|
||||
|
||||
XCTAssertEqual(ingestedIds.count, feedIds.count)
|
||||
XCTAssertEqual(ingestedTitles.count, feedTitles.count)
|
||||
|
||||
let missingIds = feedIds.subtracting(ingestedIds)
|
||||
let missingTitles = feedTitles.subtracting(ingestedTitles)
|
||||
|
||||
XCTAssertTrue(missingIds.isEmpty, "Failed to ingest feeds with these ids.")
|
||||
XCTAssertTrue(missingTitles.isEmpty, "Failed to ingest feeds with these titles.")
|
||||
|
||||
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: <)]
|
||||
}
|
||||
|
||||
let ingestedFolderAndFeedIds = (account.folders ?? Set())
|
||||
.sorted { $0.externalID! < $1.externalID! }
|
||||
.compactMap { folder -> [String: [String]]? in
|
||||
return [folder.externalID!: folder.topLevelWebFeeds.map { $0.webFeedID }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds to their corresponding folders.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// FeedlyGetCollectionsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 21/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import os.log
|
||||
|
||||
class FeedlyGetCollectionsOperationTests: XCTestCase {
|
||||
|
||||
func testGetCollections() {
|
||||
let support = FeedlyTestSupport()
|
||||
let (transport, caller) = support.makeMockNetworkStack()
|
||||
let jsonName = "feedly_collections_initial"
|
||||
transport.testFiles["/v3/collections"] = "\(jsonName).json"
|
||||
|
||||
let getCollections = FeedlyGetCollectionsOperation(service: caller, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getCollections.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getCollections)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let collections = support.testJSON(named: jsonName) as! [[String:Any]]
|
||||
let labelsInJSON = Set(collections.map { $0["label"] as! String })
|
||||
let idsInJSON = Set(collections.map { $0["id"] as! String })
|
||||
|
||||
let labels = Set(getCollections.collections.map { $0.label })
|
||||
let ids = Set(getCollections.collections.map { $0.id })
|
||||
|
||||
let missingLabels = labelsInJSON.subtracting(labels)
|
||||
let missingIds = idsInJSON.subtracting(ids)
|
||||
|
||||
XCTAssertEqual(getCollections.collections.count, collections.count, "Mismatch between collections provided by operation and test JSON collections.")
|
||||
XCTAssertTrue(missingLabels.isEmpty, "Collections with these labels did not have a corresponding \(FeedlyCollection.self) value with the same name.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding \(FeedlyCollection.self) with the same id.")
|
||||
|
||||
for collection in collections {
|
||||
let collectionId = collection["id"] as! String
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
|
||||
for operationCollection in getCollections.collections where operationCollection.id == collectionId {
|
||||
let feedIds = Set(operationCollection.feeds.map { $0.id })
|
||||
let missingIds = collectionFeedIds.subtracting(feedIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "Feeds with these ids were not found in the \"\(operationCollection.label)\" \(FeedlyCollection.self).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetCollectionsError() {
|
||||
|
||||
class TestDelegate: FeedlyOperationDelegate {
|
||||
var errorExpectation: XCTestExpectation?
|
||||
var error: Error?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
self.error = error
|
||||
errorExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
let delegate = TestDelegate()
|
||||
delegate.errorExpectation = expectation(description: "Did Fail With Expected Error")
|
||||
|
||||
let support = FeedlyTestSupport()
|
||||
let service = TestGetCollectionsService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getCollections = FeedlyGetCollectionsOperation(service: service, log: support.log)
|
||||
getCollections.delegate = delegate
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getCollections.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getCollections)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNotNil(delegate.error)
|
||||
XCTAssertTrue(getCollections.collections.isEmpty, "Collections should be empty.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// FeedlyGetStreamContentsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 23/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyGetStreamContentsOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testGetStreamContentsFailure() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: service, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
service.mockResult = .failure(URLError(.fileDoesNotExist))
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNil(getStreamContents.stream)
|
||||
}
|
||||
|
||||
func testValuesPassingForGetStreamContents() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuation: String? = "abcdefg"
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 86)
|
||||
let unreadOnly: Bool? = true
|
||||
|
||||
let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: service, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly)
|
||||
|
||||
let mockStream = FeedlyStream(id: "stream/1", updated: nil, continuation: nil, items: [])
|
||||
service.mockResult = .success(mockStream)
|
||||
service.getStreamContentsExpectation = expectation(description: "Did Call Service")
|
||||
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
||||
// Verify these values given to the opeartion are passed to the service.
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceContinuation, continuation)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertEqual(serviceUnreadOnly, unreadOnly)
|
||||
}
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
guard let stream = getStreamContents.stream else {
|
||||
XCTFail("\(FeedlyGetStreamContentsOperation.self) did not store the stream.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(stream.id, mockStream.id)
|
||||
XCTAssertEqual(stream.updated, mockStream.updated)
|
||||
XCTAssertEqual(stream.continuation, mockStream.continuation)
|
||||
|
||||
let streamIds = stream.items.map { $0.id }
|
||||
let mockStreamIds = mockStream.items.map { $0.id }
|
||||
XCTAssertEqual(streamIds, mockStreamIds)
|
||||
}
|
||||
|
||||
func testGetStreamContentsFromJSON() {
|
||||
let support = FeedlyTestSupport()
|
||||
let (transport, caller) = support.makeMockNetworkStack()
|
||||
let jsonName = "feedly_macintosh_initial"
|
||||
transport.testFiles["/v3/streams/contents"] = "\(jsonName).json"
|
||||
|
||||
let resource = FeedlyCategoryResourceId(id: "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815")
|
||||
let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: caller, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
// verify entry providing and parsed item providing
|
||||
guard let stream = getStreamContents.stream else {
|
||||
return XCTFail("Expected to have stream.")
|
||||
}
|
||||
|
||||
let streamJSON = support.testJSON(named: jsonName) as! [String:Any]
|
||||
|
||||
let id = streamJSON["id"] as! String
|
||||
XCTAssertEqual(stream.id, id)
|
||||
|
||||
let milliseconds = streamJSON["updated"] as! Double
|
||||
let updated = Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000))
|
||||
XCTAssertEqual(stream.updated, updated)
|
||||
|
||||
let continuation = streamJSON["continuation"] as! String
|
||||
XCTAssertEqual(stream.continuation, continuation)
|
||||
|
||||
support.check(getStreamContents.entries, correspondToStreamItemsIn: streamJSON)
|
||||
support.check(stream.items, correspondToStreamItemsIn: streamJSON)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// FeedlyGetStreamIdsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 23/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyGetStreamIdsOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testGetStreamIdsFailure() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: service, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
service.mockResult = .failure(URLError(.fileDoesNotExist))
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamIds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamIds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNil(getStreamIds.streamIds)
|
||||
}
|
||||
|
||||
func testValuesPassingForGetStreamIds() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuation: String? = "gfdsa"
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 1000)
|
||||
let unreadOnly: Bool? = false
|
||||
|
||||
let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: service, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly)
|
||||
|
||||
let mockStreamIds = FeedlyStreamIds(continuation: "1234", ids: ["item/1", "item/2", "item/3"])
|
||||
service.mockResult = .success(mockStreamIds)
|
||||
service.getStreamIdsExpectation = expectation(description: "Did Call Service")
|
||||
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
||||
// Verify these values given to the opeartion are passed to the service.
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceContinuation, continuation)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertEqual(serviceUnreadOnly, unreadOnly)
|
||||
}
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamIds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamIds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
guard let streamIds = getStreamIds.streamIds else {
|
||||
XCTFail("\(FeedlyGetStreamIdsOperation.self) did not store the stream.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(streamIds.continuation, mockStreamIds.continuation)
|
||||
XCTAssertEqual(streamIds.ids, mockStreamIds.ids)
|
||||
}
|
||||
|
||||
func testGetStreamIdsFromJSON() {
|
||||
let support = FeedlyTestSupport()
|
||||
let (transport, caller) = support.makeMockNetworkStack()
|
||||
let jsonName = "feedly_unreads_1000"
|
||||
transport.testFiles["/v3/streams/ids"] = "\(jsonName).json"
|
||||
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: caller, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamIds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamIds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
guard let streamIds = getStreamIds.streamIds else {
|
||||
return XCTFail("Expected to have a stream of identifiers.")
|
||||
}
|
||||
|
||||
let streamIdsJSON = support.testJSON(named: jsonName) as! [String:Any]
|
||||
|
||||
let continuation = streamIdsJSON["continuation"] as! String
|
||||
XCTAssertEqual(streamIds.continuation, continuation)
|
||||
XCTAssertEqual(streamIds.ids, streamIdsJSON["ids"] as! [String])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
//
|
||||
// FeedlyLogoutOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 15/11/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyLogoutOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
private func getTokens(for account: Account) throws -> (accessToken: Credentials, refreshToken: Credentials) {
|
||||
guard let accessToken = try account.retrieveCredentials(type: .oauthAccessToken), let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) else {
|
||||
XCTFail("Unable to retrieve access and/or refresh token from account.")
|
||||
throw CredentialsError.incompleteCredentials
|
||||
}
|
||||
return (accessToken, refreshToken)
|
||||
}
|
||||
|
||||
class TestFeedlyLogoutService: FeedlyLogoutService {
|
||||
var mockResult: Result<Void, Error>?
|
||||
var logoutExpectation: XCTestExpectation?
|
||||
|
||||
func logout(completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.logoutExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCancel() {
|
||||
let service = TestFeedlyLogoutService()
|
||||
service.logoutExpectation = expectation(description: "Did Call Logout")
|
||||
service.logoutExpectation?.isInverted = true
|
||||
|
||||
let accessToken: Credentials
|
||||
let refreshToken: Credentials
|
||||
do {
|
||||
(accessToken, refreshToken) = try getTokens(for: account)
|
||||
} catch {
|
||||
XCTFail("Could not retrieve credentials to verify their integrity later.")
|
||||
return
|
||||
}
|
||||
|
||||
let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
logout.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(logout)
|
||||
|
||||
logout.cancel()
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
XCTAssertTrue(logout.isCancelled)
|
||||
XCTAssertTrue(logout.isFinished)
|
||||
|
||||
do {
|
||||
let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken)
|
||||
let accountRefreshToken = try account.retrieveCredentials(type: .oauthRefreshToken)
|
||||
|
||||
XCTAssertEqual(accountAccessToken, accessToken)
|
||||
XCTAssertEqual(accountRefreshToken, refreshToken)
|
||||
} catch {
|
||||
XCTFail("Could not verify tokens were left intact. Did the operation delete them?")
|
||||
}
|
||||
}
|
||||
|
||||
func testLogoutSuccess() {
|
||||
let service = TestFeedlyLogoutService()
|
||||
service.logoutExpectation = expectation(description: "Did Call Logout")
|
||||
service.mockResult = .success(())
|
||||
|
||||
let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
logout.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(logout)
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
XCTAssertFalse(logout.isCancelled)
|
||||
|
||||
do {
|
||||
let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken)
|
||||
let accountRefreshToken = try account.retrieveCredentials(type: .oauthRefreshToken)
|
||||
|
||||
XCTAssertNil(accountAccessToken)
|
||||
XCTAssertNil(accountRefreshToken)
|
||||
} catch {
|
||||
XCTFail("Could not verify tokens were deleted.")
|
||||
}
|
||||
}
|
||||
|
||||
class TestLogoutDelegate: FeedlyOperationDelegate {
|
||||
var error: Error?
|
||||
var didFailExpectation: XCTestExpectation?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
self.error = error
|
||||
didFailExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testLogoutMissingAccessToken() {
|
||||
support.removeCredentials(matching: .oauthAccessToken, from: account)
|
||||
|
||||
let (_, service) = support.makeMockNetworkStack()
|
||||
service.credentials = nil
|
||||
|
||||
let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log)
|
||||
|
||||
let delegate = TestLogoutDelegate()
|
||||
delegate.didFailExpectation = expectation(description: "Did Fail")
|
||||
|
||||
logout.delegate = delegate
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
logout.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(logout)
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
XCTAssertFalse(logout.isCancelled)
|
||||
|
||||
do {
|
||||
let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken)
|
||||
XCTAssertNil(accountAccessToken)
|
||||
} catch {
|
||||
XCTFail("Could not verify tokens were deleted.")
|
||||
}
|
||||
|
||||
XCTAssertNotNil(delegate.error, "Should have failed with error.")
|
||||
if let error = delegate.error {
|
||||
switch error {
|
||||
case CredentialsError.incompleteCredentials:
|
||||
break
|
||||
default:
|
||||
XCTFail("Expected \(CredentialsError.incompleteCredentials)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testLogoutFailure() {
|
||||
let service = TestFeedlyLogoutService()
|
||||
service.logoutExpectation = expectation(description: "Did Call Logout")
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let accessToken: Credentials
|
||||
let refreshToken: Credentials
|
||||
do {
|
||||
(accessToken, refreshToken) = try getTokens(for: account)
|
||||
} catch {
|
||||
XCTFail("Could not retrieve credentials to verify their integrity later.")
|
||||
return
|
||||
}
|
||||
|
||||
let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
logout.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(logout)
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
XCTAssertFalse(logout.isCancelled)
|
||||
|
||||
do {
|
||||
let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken)
|
||||
let accountRefreshToken = try account.retrieveCredentials(type: .oauthRefreshToken)
|
||||
|
||||
XCTAssertEqual(accountAccessToken, accessToken)
|
||||
XCTAssertEqual(accountRefreshToken, refreshToken)
|
||||
} catch {
|
||||
XCTFail("Could not verify tokens were left intact. Did the operation delete them?")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
//
|
||||
// FeedlyMirrorCollectionsAsFoldersOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 22/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyMirrorCollectionsAsFoldersOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
class CollectionsProvider: FeedlyCollectionProviding {
|
||||
var collections = [
|
||||
FeedlyCollection(feeds: [], label: "One", id: "collections/1"),
|
||||
FeedlyCollection(feeds: [], label: "Two", id: "collections/2")
|
||||
]
|
||||
}
|
||||
|
||||
func testAddsFolders() {
|
||||
let provider = CollectionsProvider()
|
||||
let mirrorOperation = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
mirrorOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(mirrorOperation.collectionsAndFolders.isEmpty)
|
||||
XCTAssertTrue(mirrorOperation.feedsAndFolders.isEmpty)
|
||||
|
||||
OperationQueue.main.addOperation(mirrorOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.nameForDisplay })
|
||||
let folderExternalIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let collectionLabels = Set(provider.collections.map { $0.label })
|
||||
let collectionIds = Set(provider.collections.map { $0.id })
|
||||
|
||||
let missingNames = collectionLabels.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderExternalIds)
|
||||
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these labels have no corresponding folder.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids have no corresponding folder.")
|
||||
XCTAssertEqual(mirrorOperation.collectionsAndFolders.count, provider.collections.count, "Mismatch between collections and folders.")
|
||||
}
|
||||
|
||||
func testRemovesFolders() {
|
||||
let provider = CollectionsProvider()
|
||||
|
||||
do {
|
||||
let addFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
addFolders.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(addFolders)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
// Now that the folders are added, remove them all.
|
||||
provider.collections = []
|
||||
|
||||
let removeFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
removeFolders.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(removeFolders)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.nameForDisplay })
|
||||
let folderExternalIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let collectionLabels = Set(provider.collections.map { $0.label })
|
||||
let collectionIds = Set(provider.collections.map { $0.id })
|
||||
|
||||
let remainingNames = folderNames.subtracting(collectionLabels)
|
||||
let remainingIds = folderExternalIds.subtracting(collectionIds)
|
||||
|
||||
XCTAssertTrue(remainingNames.isEmpty, "Folders with these names remain with no corresponding collection.")
|
||||
XCTAssertTrue(remainingIds.isEmpty, "Folders with these ids remain with no corresponding collection.")
|
||||
|
||||
XCTAssertTrue(removeFolders.collectionsAndFolders.isEmpty)
|
||||
XCTAssertTrue(removeFolders.feedsAndFolders.isEmpty)
|
||||
}
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
var collections: [FeedlyCollection] {
|
||||
return [
|
||||
FeedlyCollection(feeds: feedsForCollectionOne, label: "One", id: "collections/1"),
|
||||
FeedlyCollection(feeds: feedsForCollectionTwo, label: "Two", id: "collections/2")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedMappedToFolders() {
|
||||
let provider = CollectionsAndFeedsProvider()
|
||||
let mirrorOperation = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
mirrorOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(mirrorOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.nameForDisplay })
|
||||
let folderExternalIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let collectionLabels = Set(provider.collections.map { $0.label })
|
||||
let collectionIds = Set(provider.collections.map { $0.id })
|
||||
|
||||
let missingNames = collectionLabels.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderExternalIds)
|
||||
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these labels have no corresponding folder.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids have no corresponding folder.")
|
||||
|
||||
let collectionIdsAndFeedIds = provider.collections.map { collection -> [String:[String]] in
|
||||
return [collection.id: collection.feeds.map { $0.id }.sorted(by: <)]
|
||||
}
|
||||
|
||||
let folderIdsAndFeedIds = mirrorOperation.feedsAndFolders.compactMap { feeds, folder -> [String:[String]]? in
|
||||
guard let id = folder.externalID else {
|
||||
return nil
|
||||
}
|
||||
return [id: feeds.map { $0.id }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(collectionIdsAndFeedIds, folderIdsAndFeedIds, "Did not map folders to feeds correctly.")
|
||||
}
|
||||
|
||||
func testRemovingFolderRemovesFeeds() {
|
||||
do {
|
||||
let provider = CollectionsAndFeedsProvider()
|
||||
let addFoldersAndFeeds = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
|
||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addFoldersAndFeeds, log: support.log)
|
||||
createFeeds.addDependency(addFoldersAndFeeds)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
createFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperations([addFoldersAndFeeds, createFeeds], waitUntilFinished: false)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertFalse(account.flattenedWebFeeds().isEmpty, "Expected account to have feeds.")
|
||||
}
|
||||
|
||||
// Now that the folders are added, remove them all.
|
||||
let provider = CollectionsProvider()
|
||||
provider.collections = []
|
||||
|
||||
let removeFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
removeFolders.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(removeFolders)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feeds = account.flattenedWebFeeds()
|
||||
|
||||
XCTAssertTrue(feeds.isEmpty)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
//
|
||||
// FeedlyOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 21/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyOperationTests: XCTestCase {
|
||||
|
||||
enum TestOperationError: Error, Equatable {
|
||||
case mockError
|
||||
case anotherMockError
|
||||
}
|
||||
|
||||
final class TestOperation: FeedlyOperation {
|
||||
var didCallMainExpectation: XCTestExpectation?
|
||||
var mockError: Error?
|
||||
|
||||
override func main() {
|
||||
// Should always call on main thread.
|
||||
XCTAssertTrue(Thread.isMainThread)
|
||||
|
||||
didCallMainExpectation?.fulfill()
|
||||
|
||||
if let error = mockError {
|
||||
didFinish(error)
|
||||
} else {
|
||||
didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class TestDelegate: FeedlyOperationDelegate {
|
||||
|
||||
var error: Error?
|
||||
var didFailExpectation: XCTestExpectation?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
didFailExpectation?.fulfill()
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
func testDoesCallMain() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testDoesFail() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
testOperation.mockError = TestOperationError.mockError
|
||||
|
||||
let delegate = TestDelegate()
|
||||
delegate.didFailExpectation = expectation(description: "Operation Failed As Expected")
|
||||
|
||||
testOperation.delegate = delegate
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
if let error = delegate.error as? TestOperationError {
|
||||
XCTAssertEqual(error, TestOperationError.mockError)
|
||||
} else {
|
||||
XCTFail("Expected \(TestOperationError.self) but got \(String(describing: delegate.error)).")
|
||||
}
|
||||
}
|
||||
|
||||
func testOperationFlags() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
let completionExpectation = expectation(description: "Operation Completed")
|
||||
testOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertFalse(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertFalse(testOperation.isCancelled)
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertTrue(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertFalse(testOperation.isCancelled)
|
||||
}
|
||||
|
||||
func testOperationCancellationFlags() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
testOperation.didCallMainExpectation?.isInverted = true
|
||||
|
||||
let completionExpectation = expectation(description: "Operation Completed")
|
||||
testOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertFalse(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertFalse(testOperation.isCancelled)
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
testOperation.cancel()
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertTrue(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertTrue(testOperation.isCancelled)
|
||||
}
|
||||
|
||||
func testDependency() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
let dependencyExpectation = expectation(description: "Did Call Dependency")
|
||||
let blockOperation = BlockOperation {
|
||||
dependencyExpectation.fulfill()
|
||||
}
|
||||
|
||||
blockOperation.addDependency(testOperation)
|
||||
|
||||
XCTAssertTrue(blockOperation.dependencies.contains(testOperation))
|
||||
|
||||
OperationQueue.main.addOperations([testOperation, blockOperation], waitUntilFinished: false)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// FeedlyOrganiseParsedItemsByFeedOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 24/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSParser
|
||||
|
||||
class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
struct TestParsedItemsProvider: FeedlyParsedItemProviding {
|
||||
var resource: FeedlyResourceId
|
||||
var parsedEntries: Set<ParsedItem>
|
||||
}
|
||||
|
||||
func testNoEntries() {
|
||||
let entries = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let parsedEntries = Set(entries.values.flatMap { $0 })
|
||||
let provider = TestParsedItemsProvider(resource: resource, parsedEntries: parsedEntries)
|
||||
|
||||
let organise = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
organise.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(organise)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
|
||||
func testGroupsOneEntryByFeedId() {
|
||||
let entries = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let parsedEntries = Set(entries.values.flatMap { $0 })
|
||||
let provider = TestParsedItemsProvider(resource: resource, parsedEntries: parsedEntries)
|
||||
|
||||
let organise = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
organise.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(organise)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
|
||||
func testGroupsManyEntriesByFeedId() {
|
||||
let entries = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let parsedEntries = Set(entries.values.flatMap { $0 })
|
||||
let provider = TestParsedItemsProvider(resource: resource, parsedEntries: parsedEntries)
|
||||
|
||||
let organise = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
organise.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(organise)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
//
|
||||
// FeedlyRefreshAccessTokenOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 4/11/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSWeb
|
||||
|
||||
class FeedlyRefreshAccessTokenOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
class TestRefreshTokenService: OAuthAccessTokenRefreshing {
|
||||
var mockResult: Result<OAuthAuthorizationGrant, Error>?
|
||||
var refreshAccessTokenExpectation: XCTestExpectation?
|
||||
var parameterTester: ((String, OAuthAuthorizationClient) -> ())?
|
||||
|
||||
func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
|
||||
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(refreshToken, client)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.refreshAccessTokenExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCancel() {
|
||||
let service = TestRefreshTokenService()
|
||||
service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh")
|
||||
service.refreshAccessTokenExpectation?.isInverted = true
|
||||
|
||||
let client = support.makeMockOAuthClient()
|
||||
let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
refresh.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(refresh)
|
||||
|
||||
refresh.cancel()
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
XCTAssertTrue(refresh.isCancelled)
|
||||
}
|
||||
|
||||
class TestRefreshTokenDelegate: FeedlyOperationDelegate {
|
||||
var error: Error?
|
||||
var didFailExpectation: XCTestExpectation?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
self.error = error
|
||||
didFailExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testMissingRefreshToken() {
|
||||
support.removeCredentials(matching: .oauthRefreshToken, from: account)
|
||||
|
||||
let service = TestRefreshTokenService()
|
||||
service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh")
|
||||
service.refreshAccessTokenExpectation?.isInverted = true
|
||||
|
||||
let client = support.makeMockOAuthClient()
|
||||
let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log)
|
||||
|
||||
let delegate = TestRefreshTokenDelegate()
|
||||
delegate.didFailExpectation = expectation(description: "Did Fail")
|
||||
refresh.delegate = delegate
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
refresh.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(refresh)
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
XCTAssertNotNil(delegate.error, "Should have failed with error.")
|
||||
if let error = delegate.error {
|
||||
switch error {
|
||||
case let error as TransportError:
|
||||
switch error {
|
||||
case .httpError(status: let status):
|
||||
XCTAssertEqual(status, 403, "Expected 403 Forbidden.")
|
||||
default:
|
||||
XCTFail("Expected 403 Forbidden")
|
||||
}
|
||||
default:
|
||||
XCTFail("Expected \(TransportError.httpError(status: 403))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRefreshTokenSuccess() {
|
||||
let service = TestRefreshTokenService()
|
||||
service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh")
|
||||
|
||||
let mockAccessToken = Credentials(type: .oauthAccessToken, username: "Test", secret: UUID().uuidString)
|
||||
let mockRefreshToken = Credentials(type: .oauthRefreshToken, username: "Test", secret: UUID().uuidString)
|
||||
let grant = OAuthAuthorizationGrant(accessToken: mockAccessToken, refreshToken: mockRefreshToken)
|
||||
service.mockResult = .success(grant)
|
||||
|
||||
let client = support.makeMockOAuthClient()
|
||||
service.parameterTester = { serviceRefreshToken, serviceClient in
|
||||
if let accountRefreshToken = try! self.account.retrieveCredentials(type: .oauthRefreshToken) {
|
||||
XCTAssertEqual(serviceRefreshToken, accountRefreshToken.secret)
|
||||
} else {
|
||||
XCTFail("Could not verify correct refresh token used.")
|
||||
}
|
||||
XCTAssertEqual(serviceClient, client)
|
||||
}
|
||||
|
||||
let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
refresh.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(refresh)
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
do {
|
||||
let accessToken = try account.retrieveCredentials(type: .oauthAccessToken)
|
||||
XCTAssertEqual(accessToken, mockAccessToken)
|
||||
|
||||
let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken)
|
||||
XCTAssertEqual(refreshToken, mockRefreshToken)
|
||||
} catch {
|
||||
XCTFail("Could not verify refresh and access tokens because \(error).")
|
||||
}
|
||||
}
|
||||
|
||||
func testRefreshTokenFailure() {
|
||||
let accessTokenBefore: Credentials
|
||||
let refreshTokenBefore: Credentials
|
||||
|
||||
do {
|
||||
guard let accessToken = try account.retrieveCredentials(type: .oauthAccessToken),
|
||||
let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) else {
|
||||
XCTFail("Initial refresh and/or access token does not exist.")
|
||||
return
|
||||
}
|
||||
accessTokenBefore = accessToken
|
||||
refreshTokenBefore = refreshToken
|
||||
} catch {
|
||||
XCTFail("Caught error getting initial refresh and access tokens because \(error).")
|
||||
return
|
||||
}
|
||||
|
||||
let service = TestRefreshTokenService()
|
||||
service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh")
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let client = support.makeMockOAuthClient()
|
||||
service.parameterTester = { serviceRefreshToken, serviceClient in
|
||||
if let accountRefreshToken = try! self.account.retrieveCredentials(type: .oauthRefreshToken) {
|
||||
XCTAssertEqual(serviceRefreshToken, accountRefreshToken.secret)
|
||||
} else {
|
||||
XCTFail("Could not verify correct refresh token used.")
|
||||
}
|
||||
XCTAssertEqual(serviceClient, client)
|
||||
}
|
||||
|
||||
let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
refresh.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(refresh)
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
|
||||
do {
|
||||
let accessToken = try account.retrieveCredentials(type: .oauthAccessToken)
|
||||
XCTAssertEqual(accessToken, accessTokenBefore)
|
||||
|
||||
let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken)
|
||||
XCTAssertEqual(refreshToken, refreshTokenBefore)
|
||||
} catch {
|
||||
XCTFail("Could not verify refresh and access tokens because \(error).")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,401 @@
|
|||
//
|
||||
// FeedlySendArticleStatusesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 25/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import SyncDatabase
|
||||
import Articles
|
||||
|
||||
class FeedlySendArticleStatusesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
private var container: FeedlyTestSupport.TestDatabaseContainer!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
container = support.makeTestDatabaseContainer()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
container = nil
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSendEmpty() {
|
||||
let service = TestMarkArticlesService()
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSendUnreadSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unread)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendUnreadFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unread)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendReadSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .read)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendReadFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .read)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendStarredSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .saved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendStarredFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .saved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendUnstarredSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unsaved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendUnstarredFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unsaved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendAllSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let keys = [ArticleStatus.Key.read, .starred]
|
||||
let flags = [true, false]
|
||||
let statuses = articleIds.map { articleId -> SyncStatus in
|
||||
let key = keys.randomElement()!
|
||||
let flag = flags.randomElement()!
|
||||
let status = SyncStatus(articleID: articleId, key: key, flag: flag)
|
||||
return status
|
||||
}
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
let syncStatuses: [SyncStatus]
|
||||
switch action {
|
||||
case .read:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == true }
|
||||
case .unread:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == false }
|
||||
case .saved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == true }
|
||||
case .unsaved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == false }
|
||||
}
|
||||
let expectedArticleIds = Set(syncStatuses.map { $0.articleID })
|
||||
XCTAssertEqual(serviceArticleIds, expectedArticleIds)
|
||||
}
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendAllFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let keys = [ArticleStatus.Key.read, .starred]
|
||||
let flags = [true, false]
|
||||
let statuses = articleIds.map { articleId -> SyncStatus in
|
||||
let key = keys.randomElement()!
|
||||
let flag = flags.randomElement()!
|
||||
let status = SyncStatus(articleID: articleId, key: key, flag: flag)
|
||||
return status
|
||||
}
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
let syncStatuses: [SyncStatus]
|
||||
switch action {
|
||||
case .read:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == true }
|
||||
case .unread:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == false }
|
||||
case .saved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == true }
|
||||
case .unsaved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == false }
|
||||
}
|
||||
let expectedArticleIds = Set(syncStatuses.map { $0.articleID })
|
||||
XCTAssertEqual(serviceArticleIds, expectedArticleIds)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
//
|
||||
// FeedlySetStarredArticlesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 25/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSParser
|
||||
|
||||
class FeedlySetStarredArticlesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Ensuring Unread Status By Id
|
||||
|
||||
struct TestStarredArticleProvider: FeedlyStarredEntryIdProviding {
|
||||
var entryIds: Set<String>
|
||||
}
|
||||
|
||||
func testEmptyArticleIds() {
|
||||
let testIds = Set<String>()
|
||||
let provider = TestStarredArticleProvider(entryIds: testIds)
|
||||
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertTrue(accountArticlesIDs.isEmpty)
|
||||
XCTAssertEqual(accountArticlesIDs, testIds)
|
||||
}
|
||||
|
||||
func testSetOneArticleIdStarred() {
|
||||
let testIds = Set<String>(["feed/0/article/0"])
|
||||
let provider = TestStarredArticleProvider(entryIds: testIds)
|
||||
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
func testSetManyArticleIdsStarred() {
|
||||
let testIds = Set<String>((0..<10_000).map { "feed/0/article/\($0)" })
|
||||
let provider = TestStarredArticleProvider(entryIds: testIds)
|
||||
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
func testSetSomeArticleIdsUnstarred() {
|
||||
let initialStarredIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestStarredArticleProvider(entryIds: initialStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingStarredIds = Set(initialStarredIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let remainingAccountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
|
||||
}
|
||||
|
||||
func testSetAllArticleIdsUnstarred() {
|
||||
let initialStarredIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestStarredArticleProvider(entryIds: initialStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingStarredIds = Set<String>()
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let remainingAccountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
|
||||
}
|
||||
|
||||
// MARK: - Updating Article Unread Status
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testSetAllArticlesStarred() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
func testSetManyArticlesUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let unreadItems = testItems
|
||||
.enumerated()
|
||||
.filter { $0.offset % 2 > 0 }
|
||||
.map { $0.element }
|
||||
|
||||
let remainingStarredIds = Set(unreadItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(unreadItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
func testSetOneArticleUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
// Since the test data is completely under the developer's control, not having at least one can be a programmer error.
|
||||
let remainingStarredIds = Set([testItems.compactMap { $0.syncServiceID }.first!])
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
func testSetNoArticlesRead() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingStarredIds = Set<String>()
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
|
||||
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
|
||||
let someRemainingStarredIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(someRemainingStarredIdsOfIngestedArticles))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, someRemainingStarredIdsOfIngestedArticles)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
//
|
||||
// FeedlySetUnreadArticlesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 24/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSParser
|
||||
|
||||
class FeedlySetUnreadArticlesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Ensuring Unread Status By Id
|
||||
|
||||
struct TestUnreadArticleIdProvider: FeedlyUnreadEntryIdProviding {
|
||||
var entryIds: Set<String>
|
||||
}
|
||||
|
||||
func testEmptyArticleIds() {
|
||||
let testIds = Set<String>()
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
|
||||
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertTrue(accountArticlesIDs.isEmpty)
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
func testSetOneArticleIdUnread() {
|
||||
let testIds = Set<String>(["feed/0/article/0"])
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
|
||||
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
func testSetManyArticleIdsUnread() {
|
||||
let testIds = Set<String>((0..<10_000).map { "feed/0/article/\($0)" })
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
|
||||
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
func testSetSomeArticleIdsRead() {
|
||||
let initialUnreadIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingUnreadIds = Set(initialUnreadIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let remainingAccountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
|
||||
}
|
||||
|
||||
func testSetAllArticleIdsRead() {
|
||||
let initialUnreadIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
|
||||
|
||||
do {
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingUnreadIds = Set<String>()
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let remainingAccountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
|
||||
}
|
||||
|
||||
// MARK: - Updating Article Unread Status
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testSetAllArticlesUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
func testSetManyArticlesUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let unreadItems = testItems
|
||||
.enumerated()
|
||||
.filter { $0.offset % 2 > 0 }
|
||||
.map { $0.element }
|
||||
|
||||
let remainingUnreadIds = Set(unreadItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(unreadItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
func testSetOneArticleUnread() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
// Since the test data is completely under the developer's control, not having at least one can be a programmer error.
|
||||
let remainingUnreadIds = Set([testItems.compactMap { $0.syncServiceID }.first!])
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
func testSetNoArticlesRead() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let remainingUnreadIds = Set<String>()
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() {
|
||||
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
|
||||
let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
|
||||
|
||||
do {
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds)
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
|
||||
let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
|
||||
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
setUnread.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(setUnread)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
|
||||
let someRemainingUnreadIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(someRemainingUnreadIdsOfIngestedArticles))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, someRemainingUnreadIdsOfIngestedArticles)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// FeedlySyncAllMockResponseProvider.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 1/11/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FeedlyMockResponseProvider: TestTransportMockResponseProviding {
|
||||
|
||||
let subdirectory: String
|
||||
|
||||
init(findingMocksIn subdirectory: String) {
|
||||
self.subdirectory = subdirectory
|
||||
}
|
||||
|
||||
func mockResponseFileUrl(for components: URLComponents) -> URL? {
|
||||
let bundle = Bundle(for: FeedlyMockResponseProvider.self)
|
||||
|
||||
// Match request for collections to build a list of folders.
|
||||
if components.path.contains("v3/collections") {
|
||||
return bundle.url(forResource: "collections", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
guard let queryItems = components.queryItems else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match requests for starred articles from global.saved.
|
||||
if components.path.contains("streams/contents") &&
|
||||
queryItems.contains(where: { ($0.value ?? "").contains("global.saved") }) {
|
||||
return bundle.url(forResource: "starred", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
let continuation = queryItems.first(where: { $0.name.contains("continuation") })?.value
|
||||
|
||||
// Match requests for unread article ids.
|
||||
if components.path.contains("streams/ids") && queryItems.contains(where: { $0.name.contains("unreadOnly") }) {
|
||||
|
||||
// if there is a continuation, return the page for it
|
||||
if let continuation = continuation, let data = continuation.data(using: .utf8) {
|
||||
let base64 = data.base64EncodedString() // at least base64 can be used as a path component.
|
||||
return bundle.url(forResource: "unreadIds@\(base64)", withExtension: "json", subdirectory: subdirectory)
|
||||
|
||||
} else {
|
||||
// return first page
|
||||
return bundle.url(forResource: "unreadIds", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
}
|
||||
|
||||
// Match requests for the contents of global.all.
|
||||
if components.path.contains("streams/contents") &&
|
||||
queryItems.contains(where: { ($0.value ?? "").contains("global.all") }){
|
||||
|
||||
// if there is a continuation, return the page for it
|
||||
if let continuation = continuation, let data = continuation.data(using: .utf8) {
|
||||
let base64 = data.base64EncodedString() // at least base64 can be used as a path component.
|
||||
return bundle.url(forResource: "global.all@\(base64)", withExtension: "json", subdirectory: subdirectory)
|
||||
|
||||
} else {
|
||||
// return first page
|
||||
return bundle.url(forResource: "global.all", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
//
|
||||
// FeedlySyncAllOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSWeb
|
||||
|
||||
class FeedlySyncAllOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testCancel() {
|
||||
let markArticlesService = TestMarkArticlesService()
|
||||
markArticlesService.didMarkExpectation = expectation(description: "Set Article Statuses")
|
||||
markArticlesService.didMarkExpectation?.isInverted = true
|
||||
|
||||
let getStreamIdsService = TestGetStreamIdsService()
|
||||
getStreamIdsService.getStreamIdsExpectation = expectation(description: "Get Unread Article Identifiers")
|
||||
getStreamIdsService.getStreamIdsExpectation?.isInverted = true
|
||||
|
||||
let getCollectionsService = TestGetCollectionsService()
|
||||
getCollectionsService.getCollectionsExpectation = expectation(description: "Get User's Collections")
|
||||
getCollectionsService.getCollectionsExpectation?.isInverted = true
|
||||
|
||||
let getGlobalStreamContents = TestGetStreamContentsService()
|
||||
getGlobalStreamContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.all")
|
||||
getGlobalStreamContents.getStreamContentsExpectation?.isInverted = true
|
||||
|
||||
let getStarredContents = TestGetStreamContentsService()
|
||||
getStarredContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.saved")
|
||||
getStarredContents.getStreamContentsExpectation?.isInverted = true
|
||||
|
||||
let container = support.makeTestDatabaseContainer()
|
||||
let syncAll = FeedlySyncAllOperation(account: account,
|
||||
credentials: support.accessToken,
|
||||
lastSuccessfulFetchStartDate: nil,
|
||||
markArticlesService: markArticlesService,
|
||||
getUnreadService: getStreamIdsService,
|
||||
getCollectionsService: getCollectionsService,
|
||||
getStreamContentsService: getGlobalStreamContents,
|
||||
getStarredArticlesService: getStarredContents,
|
||||
database: container.database,
|
||||
log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncAll.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncCompletionExpectation = expectation(description: "Did Finish Sync")
|
||||
syncCompletionExpectation.isInverted = true
|
||||
syncAll.syncCompletionHandler = { result in
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Sync operation was cancelled, not successful.")
|
||||
case .failure:
|
||||
XCTFail("Sync operation should cancel silently.")
|
||||
break
|
||||
}
|
||||
syncCompletionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncAll)
|
||||
|
||||
syncAll.cancel()
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNil(syncAll.syncCompletionHandler, "Expected completion handler to be destroyed after completion.")
|
||||
}
|
||||
|
||||
private var transport = TestTransport()
|
||||
lazy var caller: FeedlyAPICaller = {
|
||||
let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
|
||||
caller.credentials = support.accessToken
|
||||
return caller
|
||||
}()
|
||||
|
||||
func testSyncing() {
|
||||
performInitialSync()
|
||||
verifyInitialSync()
|
||||
|
||||
performChangeStatuses()
|
||||
verifyChangeStatuses()
|
||||
|
||||
performChangeStatusesAgain()
|
||||
verifyChangeStatusesAgain()
|
||||
|
||||
performAddFeedsAndFolders()
|
||||
verifyAddFeedsAndFolders()
|
||||
}
|
||||
|
||||
// MARK: 1 - Initial Sync
|
||||
|
||||
private func loadMockData(inSubdirectoryNamed subdirectory: String) {
|
||||
let provider = FeedlyMockResponseProvider(findingMocksIn: subdirectory)
|
||||
transport.mockResponseFileUrlProvider = provider
|
||||
|
||||
// lastSuccessfulFetchStartDate does not matter for the test, content will always be the same.
|
||||
// It is tested in `FeedlyGetStreamContentsOperationTests`.
|
||||
let syncAll = FeedlySyncAllOperation(account: account,
|
||||
credentials: support.accessToken,
|
||||
caller: caller,
|
||||
database: databaseContainer.database,
|
||||
lastSuccessfulFetchStartDate: nil,
|
||||
log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncAll.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncAll)
|
||||
|
||||
waitForExpectations(timeout: 5)
|
||||
}
|
||||
|
||||
func performInitialSync() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-1-initial")
|
||||
}
|
||||
|
||||
func verifyInitialSync() {
|
||||
let subdirectory = "feedly-1-initial"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all@MTZkOTdkZWQ1NzM6NTE2OjUzYjgyNmEy", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOTRhOTNhZTQ6MzExOjUzYjgyNmEy", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 2 - Change Statuses
|
||||
|
||||
func performChangeStatuses() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-2-changestatuses")
|
||||
}
|
||||
|
||||
func verifyChangeStatuses() {
|
||||
let subdirectory = "feedly-2-changestatuses"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOTJkNjIwM2Q6MTEzYjpkNDUwNjA3MQ==", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 3 - Change Statuses Again
|
||||
|
||||
func performChangeStatusesAgain() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-3-changestatusesagain")
|
||||
}
|
||||
|
||||
func verifyChangeStatusesAgain() {
|
||||
let subdirectory = "feedly-3-changestatusesagain"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOGRlMjVmM2M6M2YyOmQ0NTA2MDcx", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 4 - Add Feeds and Folders
|
||||
|
||||
func performAddFeedsAndFolders() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-4-addfeedsandfolders")
|
||||
}
|
||||
|
||||
func verifyAddFeedsAndFolders() {
|
||||
let subdirectory = "feedly-4-addfeedsandfolders"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOTE3YTRlMzQ6YWZjOmQ0NTA2MDcx", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 5 - Remove Feeds and Folders
|
||||
|
||||
func performRemoveFeedsAndFolders() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-5-removefeedsandfolders")
|
||||
}
|
||||
|
||||
func verifyRemoveFeedsAndFolders() {
|
||||
let subdirectory = "feedly-5-removefeedsandfolders"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOGRlMjVmM2M6M2YxOmQ0NTA2MDcx", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: Downloading Test Data
|
||||
|
||||
var lastSuccessfulFetchStartDate: Date?
|
||||
lazy var databaseContainer: FeedlyTestSupport.TestDatabaseContainer = {
|
||||
return support.makeTestDatabaseContainer()
|
||||
}()
|
||||
|
||||
func downloadTestData() {
|
||||
let caller = FeedlyAPICaller(transport: URLSession.webserviceTransport(), api: .sandbox)
|
||||
let credentials = Credentials(type: .oauthAccessToken, username: "<#USERNAME#>", secret: "<#SECRET#>")
|
||||
caller.credentials = credentials
|
||||
|
||||
let syncAll = FeedlySyncAllOperation(account: account, credentials: credentials, caller: caller, database: databaseContainer.database, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncAll.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
lastSuccessfulFetchStartDate = Date()
|
||||
|
||||
OperationQueue.main.addOperation(syncAll)
|
||||
|
||||
waitForExpectations(timeout: 60)
|
||||
}
|
||||
|
||||
// Prefix with "test" to manually run this particular function, e.g.: func test_getTestData()
|
||||
func getTestData() {
|
||||
// Add a breakpoint on the `print` statements and start a proxy server on your Mac.
|
||||
// 1. In Feedly sandbox, perform the actions implied by the string in the print statement.
|
||||
// 2. In the proxy server app, such as Charles, clear requests and responses and filter by "sandbox".
|
||||
// 3. In Xcode, hit continue in the Debugger so the test requests the data.
|
||||
// 4. Save the responses captured by the proxy.
|
||||
print("Prepare for initial sync.")
|
||||
downloadTestData()
|
||||
|
||||
assert(lastSuccessfulFetchStartDate != nil)
|
||||
|
||||
print("Read/unread, star and unstar some articles.")
|
||||
downloadTestData()
|
||||
|
||||
print("Read/unread, star and unstar some articles again.")
|
||||
downloadTestData()
|
||||
|
||||
print("Add Feeds and Folders.")
|
||||
downloadTestData()
|
||||
|
||||
print("Rename and Remove Feeds and Folders.")
|
||||
downloadTestData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
//
|
||||
// FeedlySyncStarredArticlesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlySyncStarredArticlesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testIngestsOnePageSuccess() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let items = service.makeMockFeedlyEntryItem()
|
||||
service.mockResult = .success(FeedlyStream(id: resource.id, updated: nil, continuation: nil, items: items))
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let expectedArticleIds = Set(items.map { $0.id })
|
||||
let starredArticleIds = account.fetchStarredArticleIDs()
|
||||
let missingIds = expectedArticleIds.subtracting(starredArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.")
|
||||
|
||||
// Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to.
|
||||
let expectedArticles = account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
|
||||
let starredArticles = account.fetchArticles(.articleIDs(starredArticleIds))
|
||||
XCTAssertEqual(expectedArticleIds.count, expectedArticles.count)
|
||||
let missingArticles = expectedArticles.subtracting(starredArticles)
|
||||
XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.")
|
||||
XCTAssertEqual(expectedArticles, starredArticles)
|
||||
}
|
||||
|
||||
func testIngestsOnePageFailure() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let starredArticleIds = account.fetchStarredArticleIDs()
|
||||
XCTAssertTrue(starredArticleIds.isEmpty)
|
||||
}
|
||||
|
||||
func testIngestsManyPagesSuccess() {
|
||||
let service = TestGetPagedStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuations = (1...10).map { "\($0)" }
|
||||
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 10)
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
var remainingContinuations = Set(continuations)
|
||||
let getStreamPageExpectation = expectation(description: "Did Request Page")
|
||||
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
|
||||
if let continuation = continuation {
|
||||
XCTAssertTrue(remainingContinuations.contains(continuation))
|
||||
remainingContinuations.remove(continuation)
|
||||
}
|
||||
|
||||
getStreamPageExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStarred.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStarred)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
// Find articles inserted.
|
||||
let expectedArticleIds = Set(service.pages.values.map { $0.items }.flatMap { $0 }.map { $0.id })
|
||||
let starredArticleIds = account.fetchStarredArticleIDs()
|
||||
let missingIds = expectedArticleIds.subtracting(starredArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.")
|
||||
|
||||
// Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to.
|
||||
let expectedArticles = account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
|
||||
let starredArticles = account.fetchArticles(.articleIDs(starredArticleIds))
|
||||
XCTAssertEqual(expectedArticleIds.count, expectedArticles.count)
|
||||
let missingArticles = expectedArticles.subtracting(starredArticles)
|
||||
XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.")
|
||||
XCTAssertEqual(expectedArticles, starredArticles)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// FeedlySyncStreamContentsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 26/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlySyncStreamContentsOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testIngestsOnePageSuccess() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 0)
|
||||
let items = service.makeMockFeedlyEntryItem()
|
||||
service.mockResult = .success(FeedlyStream(id: resource.id, updated: nil, continuation: nil, items: items))
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStreamContents = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: service, newerThan: newerThan, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let expectedArticleIds = Set(items.map { $0.id })
|
||||
let expectedArticles = account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
}
|
||||
|
||||
func testIngestsOnePageFailure() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 0)
|
||||
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStreamContents = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: service, newerThan: newerThan, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testIngestsManyPagesSuccess() {
|
||||
let service = TestGetPagedStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 0)
|
||||
|
||||
let continuations = (1...10).map { "\($0)" }
|
||||
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 1000)
|
||||
|
||||
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamContentsExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
var remainingContinuations = Set(continuations)
|
||||
let getStreamPageExpectation = expectation(description: "Did Request Page")
|
||||
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
service.getStreamContentsExpectation = getStreamContentsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
|
||||
if let continuation = continuation {
|
||||
XCTAssertTrue(remainingContinuations.contains(continuation))
|
||||
remainingContinuations.remove(continuation)
|
||||
}
|
||||
|
||||
getStreamPageExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncStreamContents = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: service, newerThan: newerThan, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
|
||||
// Find articles inserted.
|
||||
let articleIds = Set(service.pages.values.map { $0.items }.flatMap { $0 }.map { $0.id })
|
||||
let articles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertEqual(articleIds.count, articles.count)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// FeedlySyncUnreadStatusesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 29/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlySyncUnreadStatusesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testIngestsOnePageSuccess() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let ids = [UUID().uuidString]
|
||||
service.mockResult = .success(FeedlyStreamIds(continuation: nil, ids: ids))
|
||||
|
||||
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Ids")
|
||||
getStreamIdsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamIdsExpectation = getStreamIdsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertEqual(serviceUnreadOnly, true)
|
||||
}
|
||||
|
||||
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncUnreads.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncUnreads)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let expectedArticleIds = Set(ids)
|
||||
let unreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
|
||||
}
|
||||
|
||||
func testIngestsOnePageFailure() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamIdsExpectation.expectedFulfillmentCount = 1
|
||||
|
||||
service.getStreamIdsExpectation = getStreamIdsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertEqual(serviceUnreadOnly, true)
|
||||
}
|
||||
|
||||
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncUnreads.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncUnreads)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let unreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
XCTAssertTrue(unreadArticleIds.isEmpty)
|
||||
}
|
||||
|
||||
func testIngestsManyPagesSuccess() {
|
||||
let service = TestGetPagedStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuations = (1...10).map { "\($0)" }
|
||||
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 1000)
|
||||
|
||||
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents")
|
||||
getStreamIdsExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
var remainingContinuations = Set(continuations)
|
||||
let getStreamPageExpectation = expectation(description: "Did Request Page")
|
||||
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
|
||||
|
||||
service.getStreamIdsExpectation = getStreamIdsExpectation
|
||||
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertNil(serviceNewerThan)
|
||||
XCTAssertEqual(serviceUnreadOnly, true)
|
||||
|
||||
if let continuation = continuation {
|
||||
XCTAssertTrue(remainingContinuations.contains(continuation))
|
||||
remainingContinuations.remove(continuation)
|
||||
}
|
||||
|
||||
getStreamPageExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncUnreads.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncUnreads)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
// Find statuses inserted.
|
||||
let expectedArticleIds = Set(service.pages.values.map { $0.ids }.flatMap { $0 })
|
||||
let unreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,292 @@
|
|||
//
|
||||
// FeedlyTestSupport.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 22/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
@testable import Account
|
||||
import os.log
|
||||
import SyncDatabase
|
||||
|
||||
class FeedlyTestSupport {
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "FeedlyTests")
|
||||
var accessToken = Credentials(type: .oauthAccessToken, username: "Test", secret: "t3st-access-tok3n")
|
||||
var refreshToken = Credentials(type: .oauthRefreshToken, username: "Test", secret: "t3st-refresh-tok3n")
|
||||
var transport = TestTransport()
|
||||
|
||||
func makeMockNetworkStack() -> (TestTransport, FeedlyAPICaller) {
|
||||
let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
|
||||
caller.credentials = accessToken
|
||||
return (transport, caller)
|
||||
}
|
||||
|
||||
func makeTestAccount() -> Account {
|
||||
let manager = TestAccountManager()
|
||||
let account = manager.createAccount(type: .feedly, transport: transport)
|
||||
do {
|
||||
try account.storeCredentials(refreshToken)
|
||||
// This must be done last or the account uses the refresh token for request Authorization!
|
||||
try account.storeCredentials(accessToken)
|
||||
} catch {
|
||||
XCTFail("Unable to register mock credentials because \(error)")
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
func makeMockOAuthClient() -> OAuthAuthorizationClient {
|
||||
return OAuthAuthorizationClient(id: "test", redirectUri: "test://test/auth", state: nil, secret: "password")
|
||||
}
|
||||
|
||||
func removeCredentials(matching type: CredentialsType, from account: Account) {
|
||||
do {
|
||||
try account.removeCredentials(type: type)
|
||||
} catch {
|
||||
XCTFail("Unable to remove \(type)")
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestDatabaseContainer() -> TestDatabaseContainer {
|
||||
return TestDatabaseContainer()
|
||||
}
|
||||
|
||||
class TestDatabaseContainer {
|
||||
private let path: String
|
||||
private(set) var database: SyncDatabase!
|
||||
|
||||
init() {
|
||||
let dataFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
path = dataFolder.appendingPathComponent("\(UUID().uuidString)-Sync.sqlite3").path
|
||||
database = SyncDatabase(databaseFilePath: path)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// We should close the database before removing the database.
|
||||
database = nil
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
print("Removed database at \(path)")
|
||||
} catch {
|
||||
print("Unable to remove database owned by \(self) because \(error).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func destroy(_ testAccount: Account) {
|
||||
do {
|
||||
// These should not throw when the keychain items are not found.
|
||||
try testAccount.removeCredentials(type: .oauthAccessToken)
|
||||
try testAccount.removeCredentials(type: .oauthRefreshToken)
|
||||
} catch {
|
||||
XCTFail("Unable to clean up mock credentials because \(error)")
|
||||
}
|
||||
|
||||
let manager = TestAccountManager()
|
||||
manager.deleteAccount(testAccount)
|
||||
}
|
||||
|
||||
func testJSON(named: String, subdirectory: String? = nil) -> Any {
|
||||
let bundle = Bundle(for: TestTransport.self)
|
||||
let url = bundle.url(forResource: named, withExtension: "json", subdirectory: subdirectory)!
|
||||
let data = try! Data(contentsOf: url)
|
||||
let json = try! JSONSerialization.jsonObject(with: data)
|
||||
return json
|
||||
}
|
||||
|
||||
func checkFoldersAndFeeds(in account: Account, againstCollectionsAndFeedsInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let collections = testJSON(named: name, subdirectory: subdirectory) as! [[String:Any]]
|
||||
let collectionNames = Set(collections.map { $0["label"] as! String })
|
||||
let collectionIds = Set(collections.map { $0["id"] as! String })
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.name })
|
||||
let folderIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let missingNames = collectionNames.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderIds)
|
||||
|
||||
XCTAssertEqual(folders.count, collections.count, "Mismatch between collections and folders.")
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these names did not have a corresponding folder with the same name.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding folder with the same id.")
|
||||
|
||||
for collection in collections {
|
||||
checkSingleFolderAndFeeds(in: account, againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(in account: Account, againstOneCollectionAndFeedsInJSONNamed name: String) {
|
||||
let collection = testJSON(named: name) as! [String:Any]
|
||||
checkSingleFolderAndFeeds(in: account, againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(in account: Account, againstOneCollectionAndFeedsInJSONPayload collection: [String: Any]) {
|
||||
let label = collection["label"] as! String
|
||||
guard let folder = account.existingFolder(with: label) else {
|
||||
// due to a previous test failure?
|
||||
XCTFail("Could not find the \"\(label)\" folder.")
|
||||
return
|
||||
}
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let folderFeeds = folder.topLevelWebFeeds
|
||||
|
||||
XCTAssertEqual(collectionFeeds.count, folderFeeds.count)
|
||||
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
let folderFeedIds = Set(folderFeeds.map { $0.webFeedID })
|
||||
let missingFeedIds = collectionFeedIds.subtracting(folderFeedIds)
|
||||
|
||||
XCTAssertTrue(missingFeedIds.isEmpty, "Feeds with these ids were not found in the \"\(label)\" folder.")
|
||||
}
|
||||
|
||||
func checkArticles(in account: Account, againstItemsInStreamInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let stream = testJSON(named: name, subdirectory: subdirectory) as! [String:Any]
|
||||
checkArticles(in: account, againstItemsInStreamInJSONPayload: stream)
|
||||
}
|
||||
|
||||
func checkArticles(in account: Account, againstItemsInStreamInJSONPayload stream: [String: Any]) {
|
||||
checkArticles(in: account, correspondToStreamItemsIn: stream)
|
||||
}
|
||||
|
||||
private struct ArticleItem {
|
||||
var id: String
|
||||
var feedId: String
|
||||
var content: String
|
||||
var JSON: [String: Any]
|
||||
var unread: Bool
|
||||
|
||||
/// 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? {
|
||||
return ((JSON["canonical"] as? [[String: Any]]) ?? (JSON["alternate"] as? [[String: Any]]))?.compactMap { link -> String? in
|
||||
let href = link["href"] as? String
|
||||
if let type = link["type"] as? String {
|
||||
if type == "text/html" {
|
||||
return href
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return href
|
||||
}.first
|
||||
}
|
||||
|
||||
init(item: [String: Any]) {
|
||||
self.JSON = item
|
||||
self.id = item["id"] as! String
|
||||
|
||||
let origin = item["origin"] as! [String: Any]
|
||||
self.feedId = origin["streamId"] as! String
|
||||
|
||||
let content = item["content"] as? [String: Any]
|
||||
let summary = item["summary"] as? [String: Any]
|
||||
self.content = ((content ?? summary)?["content"] as? String) ?? ""
|
||||
|
||||
self.unread = item["unread"] as! Bool
|
||||
}
|
||||
}
|
||||
|
||||
/// Awkwardly titled to make it clear the JSON given is from a stream response.
|
||||
func checkArticles(in testAccount: Account, correspondToStreamItemsIn stream: [String: Any]) {
|
||||
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let articleItems = items.map { ArticleItem(item: $0) }
|
||||
let itemIds = Set(articleItems.map { $0.id })
|
||||
|
||||
let articles = testAccount.fetchArticles(.articleIDs(itemIds))
|
||||
let articleIds = Set(articles.map { $0.articleID })
|
||||
|
||||
let missing = itemIds.subtracting(articleIds)
|
||||
|
||||
XCTAssertEqual(items.count, articles.count)
|
||||
XCTAssertTrue(missing.isEmpty, "Items with these ids did not have a corresponding article with the same id.")
|
||||
|
||||
for article in articles {
|
||||
for item in articleItems where item.id == article.articleID {
|
||||
XCTAssertEqual(article.uniqueID, item.id)
|
||||
XCTAssertEqual(article.contentHTML, item.content)
|
||||
XCTAssertEqual(article.webFeedID, item.feedId)
|
||||
XCTAssertEqual(article.externalURL, item.externalUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkUnreadStatuses(in account: Account, againstIdsInStreamInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let streamIds = testJSON(named: name, subdirectory: subdirectory) as! [String:Any]
|
||||
checkUnreadStatuses(in: account, correspondToIdsInJSONPayload: streamIds)
|
||||
}
|
||||
|
||||
func checkUnreadStatuses(in testAccount: Account, correspondToIdsInJSONPayload streamIds: [String: Any]) {
|
||||
let ids = Set(streamIds["ids"] as! [String])
|
||||
let articleIds = testAccount.fetchUnreadArticleIDs()
|
||||
// Unread statuses can be paged from Feedly.
|
||||
// Instead of joining test data, the best we can do is
|
||||
// make sure that these ids are marked as unread (a subset of the total).
|
||||
XCTAssertTrue(ids.isSubset(of: articleIds), "Some articles in `ids` are not marked as unread.")
|
||||
}
|
||||
|
||||
func checkStarredStatuses(in account: Account, againstItemsInStreamInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let streamIds = testJSON(named: name, subdirectory: subdirectory) as! [String:Any]
|
||||
checkStarredStatuses(in: account, correspondToStreamItemsIn: streamIds)
|
||||
}
|
||||
|
||||
func checkStarredStatuses(in testAccount: Account, correspondToStreamItemsIn stream: [String: Any]) {
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let ids = Set(items.map { $0["id"] as! String })
|
||||
let articleIds = testAccount.fetchStarredArticleIDs()
|
||||
// Starred articles can be paged from Feedly.
|
||||
// Instead of joining test data, the best we can do is
|
||||
// make sure that these articles are marked as starred (a subset of the total).
|
||||
XCTAssertTrue(ids.isSubset(of: articleIds), "Some articles in `ids` are not marked as starred.")
|
||||
}
|
||||
|
||||
func check(_ entries: [FeedlyEntry], correspondToStreamItemsIn stream: [String: Any]) {
|
||||
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let itemIds = Set(items.map { $0["id"] as! String })
|
||||
|
||||
let articleIds = Set(entries.map { $0.id })
|
||||
|
||||
let missing = itemIds.subtracting(articleIds)
|
||||
|
||||
XCTAssertEqual(items.count, entries.count)
|
||||
XCTAssertTrue(missing.isEmpty, "Failed to create \(FeedlyEntry.self) values from objects in the JSON with these ids.")
|
||||
}
|
||||
|
||||
func makeParsedItemTestDataFor(numberOfFeeds: Int, numberOfItemsInFeeds: Int) -> [String: Set<ParsedItem>] {
|
||||
let ids = (0..<numberOfFeeds).map { "feed/\($0)" }
|
||||
let feedIdsAndItemCounts = ids.map { ($0, numberOfItemsInFeeds) }
|
||||
|
||||
let entries = feedIdsAndItemCounts.map { (feedId, count) -> (String, [Int]) in
|
||||
return (feedId, (0..<count).map { $0 })
|
||||
|
||||
}.map { pair -> (String, Set<ParsedItem>) in
|
||||
let items = pair.1.map { index -> ParsedItem in
|
||||
ParsedItem(syncServiceID: "\(pair.0)/articles/\(index)",
|
||||
uniqueID: UUID().uuidString,
|
||||
feedURL: pair.0,
|
||||
url: "http://localhost/",
|
||||
externalURL: "http://localhost/\(pair.0)/articles/\(index).html",
|
||||
title: "Title\(index)",
|
||||
contentHTML: "Content \(index) HTML.",
|
||||
contentText: "Content \(index) Text",
|
||||
summary: nil,
|
||||
imageURL: nil,
|
||||
bannerImageURL: nil,
|
||||
datePublished: nil,
|
||||
dateModified: nil,
|
||||
authors: nil,
|
||||
tags: nil,
|
||||
attachments: nil)
|
||||
}
|
||||
return (pair.0, Set(items))
|
||||
}.reduce([String: Set<ParsedItem>](minimumCapacity: feedIdsAndItemCounts.count)) { (dict, pair) in
|
||||
var mutant = dict
|
||||
mutant[pair.0] = pair.1
|
||||
return mutant
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// FeedlyUpdateAccountFeedsWithItemsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 24/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSParser
|
||||
|
||||
class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testUpdateAccountWithEmptyItems() {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.isEmpty)
|
||||
}
|
||||
|
||||
func testUpdateAccountWithOneItem() {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.count == entries.count)
|
||||
|
||||
let accountArticleIds = Set(accountArticles.map { $0.articleID })
|
||||
let missingIds = articleIds.subtracting(accountArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty)
|
||||
}
|
||||
|
||||
func testUpdateAccountWithManyItems() {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
waitForExpectations(timeout: 10) // 10,000 articles takes ~ three seconds for me.
|
||||
|
||||
let entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.count == entries.count)
|
||||
|
||||
let accountArticleIds = Set(accountArticles.map { $0.articleID })
|
||||
let missingIds = articleIds.subtracting(accountArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty)
|
||||
}
|
||||
|
||||
func testCancelUpdateAccount() {
|
||||
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
|
||||
|
||||
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
update.cancel()
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.isEmpty)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","items":[]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// TestGetCollectionsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetCollectionsService: FeedlyGetCollectionsService {
|
||||
var mockResult: Result<[FeedlyCollection], Error>?
|
||||
var getCollectionsExpectation: XCTestExpectation?
|
||||
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.getCollectionsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// TestGetPagedStreamContentsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetPagedStreamContentsService: FeedlyGetStreamContentsService {
|
||||
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamContentsExpectation: XCTestExpectation?
|
||||
var pages = [String: FeedlyStream]()
|
||||
|
||||
func addAtLeastOnePage(for resource: FeedlyResourceId, continuations: [String], numberOfEntriesPerPage count: Int) {
|
||||
pages = [String: FeedlyStream](minimumCapacity: continuations.count + 1)
|
||||
|
||||
// A continuation is an identifier for the next page.
|
||||
// The first page has a nil identifier.
|
||||
// The last page has no next page, so the next continuation value for that page is nil.
|
||||
// Therefore, each page needs to know the identifier of the next page.
|
||||
for index in -1..<continuations.count {
|
||||
let nextIndex = index + 1
|
||||
let continuation: String? = nextIndex < continuations.count ? continuations[nextIndex] : nil
|
||||
let page = makeStreamContents(for: resource, continuation: continuation, between: 0..<count)
|
||||
let key = TestGetPagedStreamContentsService.getPagingKey(for: resource, continuation: index < 0 ? nil : continuations[index])
|
||||
pages[key] = page
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStreamContents(for resource: FeedlyResourceId, continuation: String?, between range: Range<Int>) -> FeedlyStream {
|
||||
let entries = range.map { index -> FeedlyEntry in
|
||||
let content = FeedlyEntry.Content(content: "Content \(index)",
|
||||
direction: .leftToRight)
|
||||
|
||||
let origin = FeedlyOrigin(title: "Origin \(index)",
|
||||
streamId: resource.id,
|
||||
htmlUrl: "http://localhost/feedly/origin/\(index)")
|
||||
|
||||
return FeedlyEntry(id: "/articles/\(index)",
|
||||
title: "Article \(index)",
|
||||
content: content,
|
||||
summary: content,
|
||||
author: nil,
|
||||
published: Date(),
|
||||
updated: nil,
|
||||
origin: origin,
|
||||
canonical: nil,
|
||||
alternate: nil,
|
||||
unread: true,
|
||||
tags: nil,
|
||||
categories: nil,
|
||||
enclosure: nil)
|
||||
}
|
||||
|
||||
let stream = FeedlyStream(id: resource.id, updated: nil, continuation: continuation, items: entries)
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
static func getPagingKey(for stream: FeedlyResourceId, continuation: String?) -> String {
|
||||
return "\(stream.id)@\(continuation ?? "")"
|
||||
}
|
||||
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
let key = TestGetPagedStreamContentsService.getPagingKey(for: resource, continuation: continuation)
|
||||
guard let page = pages[key] else {
|
||||
XCTFail("Missing page for \(resource.id) and continuation \(String(describing: continuation)). Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(.success(page))
|
||||
self.getStreamContentsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// TestGetPagedStreamIdsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 29/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetPagedStreamIdsService: FeedlyGetStreamIdsService {
|
||||
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamIdsExpectation: XCTestExpectation?
|
||||
var pages = [String: FeedlyStreamIds]()
|
||||
|
||||
func addAtLeastOnePage(for resource: FeedlyResourceId, continuations: [String], numberOfEntriesPerPage count: Int) {
|
||||
pages = [String: FeedlyStreamIds](minimumCapacity: continuations.count + 1)
|
||||
|
||||
// A continuation is an identifier for the next page.
|
||||
// The first page has a nil identifier.
|
||||
// The last page has no next page, so the next continuation value for that page is nil.
|
||||
// Therefore, each page needs to know the identifier of the next page.
|
||||
for index in -1..<continuations.count {
|
||||
let nextIndex = index + 1
|
||||
let continuation: String? = nextIndex < continuations.count ? continuations[nextIndex] : nil
|
||||
let page = makeStreamIds(for: resource, continuation: continuation, between: 0..<count)
|
||||
let key = TestGetPagedStreamIdsService.getPagingKey(for: resource, continuation: index < 0 ? nil : continuations[index])
|
||||
pages[key] = page
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStreamIds(for resource: FeedlyResourceId, continuation: String?, between range: Range<Int>) -> FeedlyStreamIds {
|
||||
let entryIds = range.map { _ in UUID().uuidString }
|
||||
let stream = FeedlyStreamIds(continuation: continuation, ids: entryIds)
|
||||
return stream
|
||||
}
|
||||
|
||||
static func getPagingKey(for stream: FeedlyResourceId, continuation: String?) -> String {
|
||||
return "\(stream.id)@\(continuation ?? "")"
|
||||
}
|
||||
|
||||
func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
let key = TestGetPagedStreamIdsService.getPagingKey(for: resource, continuation: continuation)
|
||||
guard let page = pages[key] else {
|
||||
XCTFail("Missing page for \(resource.id) and continuation \(String(describing: continuation)). Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(.success(page))
|
||||
self.getStreamIdsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// TestGetStreamContentsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetStreamContentsService: FeedlyGetStreamContentsService {
|
||||
|
||||
var mockResult: Result<FeedlyStream, Error>?
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamContentsExpectation: XCTestExpectation?
|
||||
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.getStreamContentsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func makeMockFeedlyEntryItem() -> [FeedlyEntry] {
|
||||
let origin = FeedlyOrigin(title: "XCTest@localhost", streamId: "user/12345/category/67890", htmlUrl: "http://localhost/nnw/xctest")
|
||||
let content = FeedlyEntry.Content(content: "In the beginning...", direction: .leftToRight)
|
||||
let items = [FeedlyEntry(id: "feeds/0/article/0",
|
||||
title: "RSS Reader Ingests Man",
|
||||
content: content,
|
||||
summary: content,
|
||||
author: nil,
|
||||
published: Date(),
|
||||
updated: nil,
|
||||
origin: origin,
|
||||
canonical: nil,
|
||||
alternate: nil,
|
||||
unread: true,
|
||||
tags: nil,
|
||||
categories: nil,
|
||||
enclosure: nil)]
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// TestGetStreamIdsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 29/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetStreamIdsService: FeedlyGetStreamIdsService {
|
||||
|
||||
var mockResult: Result<FeedlyStreamIds, Error>?
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamIdsExpectation: XCTestExpectation?
|
||||
|
||||
func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.getStreamIdsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// TestMarkArticlesService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class TestMarkArticlesService: FeedlyMarkArticlesService {
|
||||
|
||||
var didMarkExpectation: XCTestExpectation?
|
||||
var parameterTester: ((Set<String>, FeedlyMarkAction) -> ())?
|
||||
var mockResult: Result<Void, Error> = .success(())
|
||||
|
||||
func mark(_ articleIds: Set<String>, as action: FeedlyMarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.parameterTester?(articleIds, action)
|
||||
completionHandler(self.mockResult)
|
||||
self.didMarkExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,296 @@
|
|||
{
|
||||
"ids": [
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d947baaaa:15c6:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9444b6ae:155b:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d942c0878:2de:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93f9d9be:2ab:53b826a2",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d93d6ce8f:147d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d93d6ce8f:147c:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93c1a015:251:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93b34eba:241:53b826a2",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d93b31822:142f:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d93b31822:142e:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d93b31822:142d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d939fda8c:13f2:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93950469:21b:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93950469:21a:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d938aedbe:216:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93852581:211:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9380dda0:209:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9376eefc:202:53b826a2",
|
||||
"kv2DIas8GblflohzMAcClzUErTYUYammDtqm4auH/og=_16d936e2abe:1359:d4506071",
|
||||
"kv2DIas8GblflohzMAcClzUErTYUYammDtqm4auH/og=_16d936e2abe:1358:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d936d06f3:1ef:53b826a2",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9368e67a:133d:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d93445712:12e2:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d933ea3c0:c4:53b826a2",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9331f22f:1299:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9331f22f:1298:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d931c60da:8b:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93167a9c:7c:53b826a2",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d930e3ae3:122b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92fafd47:119c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92fafd47:119b:d4506071",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d92efb228:118b:d4506071",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d92efb228:118a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92e40b08:11:53b826a2",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d92d74711:113f:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d92d74711:113e:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d92d74711:113d:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d92d6203d:113b:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d47:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d46:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fb:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fa:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92c19339:d3b:fc4690a0",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d92c10fc0:10f0:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92ad8234:d27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92a356f6:d1f:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0b:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d926b25dc:cd9:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d9267e977:fc9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:ccf:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:cce:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d925620e1:f90:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d924895c3:cb8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cac:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cab:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:caa:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:ca9:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9e:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eef:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eee:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d920c42e7:c86:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91f7f422:c70:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91e838fb:e54:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d9837f:c5c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d3a580:c56:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c49:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c48:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c47:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d91c0ea34:dcf:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91b143b9:d96:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d919d0e1b:c27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9198dae9:c21:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3f:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3d:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3c:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3b:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3a:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b39:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b38:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b37:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b36:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b35:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c15:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c14:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c13:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afe:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afd:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afc:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9174a7bc:c02:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d90f19878:b55:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8f76c4f0:884:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8edefa36:703:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec5b3f5:6f1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec0c455:6dc:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8eafe951:655:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e7a5c11:5b8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e47ea63:4f3:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:555:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:554:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a2:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e29a65f:44f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8e19672d:4b9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e157c76:3ca:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8df74270:3a3:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f3:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f2:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d8dab6a11:2d6:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d88a4dca2:50e96:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d875a7313:503b8:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
|
||||
"updated": 1572574658046,
|
||||
"items": []
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,258 @@
|
|||
{
|
||||
"ids": [
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d47:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d46:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fb:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fa:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92c19339:d3b:fc4690a0",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d92c10fc0:10f0:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92ad8234:d27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92a356f6:d1f:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0b:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d926b25dc:cd9:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d9267e977:fc9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:ccf:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:cce:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d925620e1:f90:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d924895c3:cb8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cac:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cab:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:caa:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:ca9:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9e:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eef:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eee:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d920c42e7:c86:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91f7f422:c70:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91e838fb:e54:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d9837f:c5c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d3a580:c56:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c49:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c48:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c47:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d91c0ea34:dcf:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91b143b9:d96:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d919d0e1b:c27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9198dae9:c21:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3f:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3d:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3c:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3b:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3a:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b39:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b38:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b37:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b36:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b35:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c15:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c14:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c13:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afe:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afd:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afc:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9174a7bc:c02:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d90f19878:b55:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8f76c4f0:884:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8edefa36:703:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec5b3f5:6f1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec0c455:6dc:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8eafe951:655:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e7a5c11:5b8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e47ea63:4f3:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:555:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:554:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a2:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e29a65f:44f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8e19672d:4b9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e157c76:3ca:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8df74270:3a3:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f3:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f2:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d8dab6a11:2d6:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d88a4dca2:50e96:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d875a7313:503b8:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
|
||||
"updated": 1572574658046,
|
||||
"items": []
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"ids": [
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"ids": [
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9174a7bc:c02:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d90f19878:b55:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8f76c4f0:884:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8edefa36:703:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec5b3f5:6f1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec0c455:6dc:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8eafe951:655:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e7a5c11:5b8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e47ea63:4f3:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:555:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:554:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a2:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e29a65f:44f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8e19672d:4b9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e157c76:3ca:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8df74270:3a3:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f3:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f2:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
|
||||
"updated": 1572574658046,
|
||||
"items": []
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"ids": [
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ids": []
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,7 @@ class TestAccountManager {
|
|||
static let shared = TestAccountManager()
|
||||
|
||||
var accountsFolder: URL {
|
||||
return FileManager.default.temporaryDirectory
|
||||
return try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ class TestAccountManager {
|
|||
try FileManager.default.removeItem(atPath: account.dataFolder)
|
||||
}
|
||||
catch let error as CocoaError where error.code == .fileNoSuchFile {
|
||||
print("Unable to delete folder at: \(account.dataFolder) because \(error)")
|
||||
|
||||
}
|
||||
catch {
|
||||
assertionFailure("Could not delete folder at: \(account.dataFolder) because \(error)")
|
||||
|
|
|
@ -10,6 +10,10 @@ import Foundation
|
|||
import RSWeb
|
||||
import XCTest
|
||||
|
||||
protocol TestTransportMockResponseProviding: class {
|
||||
func mockResponseFileUrl(for components: URLComponents) -> URL?
|
||||
}
|
||||
|
||||
final class TestTransport: Transport {
|
||||
|
||||
enum TestTransportError: String, Error {
|
||||
|
@ -19,12 +23,7 @@ final class TestTransport: Transport {
|
|||
var testFiles = [String: String]()
|
||||
var testStatusCodes = [String: Int]()
|
||||
|
||||
/// Allows tests to filter time sensitive state out to make matching against test data easier.
|
||||
var blacklistedQueryItemNames = Set([
|
||||
"newerThan", // Feedly: Mock data has a fixed date.
|
||||
"unreadOnly", // Feedly: Mock data is read/unread by test expectation.
|
||||
"count", // Feedly: Mock data is limited by test expectation.
|
||||
])
|
||||
weak var mockResponseFileUrlProvider: TestTransportMockResponseProviding?
|
||||
|
||||
private func httpResponse(for request: URLRequest, statusCode: Int = 200) -> HTTPURLResponse {
|
||||
guard let url = request.url else {
|
||||
|
@ -33,43 +32,49 @@ final class TestTransport: Transport {
|
|||
return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: nil)!
|
||||
}
|
||||
|
||||
func cancelAll() { }
|
||||
|
||||
func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) {
|
||||
|
||||
guard let url = request.url, var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
completion(.failure(TestTransportError.invalidState))
|
||||
return
|
||||
}
|
||||
|
||||
components.queryItems = components
|
||||
.queryItems?
|
||||
.filter { !blacklistedQueryItemNames.contains($0.name) }
|
||||
|
||||
guard let urlString = components.url?.absoluteString else {
|
||||
guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
completion(.failure(TestTransportError.invalidState))
|
||||
return
|
||||
}
|
||||
|
||||
let urlString = url.absoluteString
|
||||
let response = httpResponse(for: request, statusCode: testStatusCodes[urlString] ?? 200)
|
||||
let testFileURL: URL
|
||||
|
||||
var mockResponseFound = false
|
||||
for (key, testFileName) in testFiles where urlString.contains(key) {
|
||||
let testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testFileName)
|
||||
let data = try! Data(contentsOf: testFileURL)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.success((response, data)))
|
||||
if let provider = mockResponseFileUrlProvider {
|
||||
guard let providerUrl = provider.mockResponseFileUrl(for: components) else {
|
||||
XCTFail("Test behaviour undefined. Mock provider failed to provide non-nil URL for \(components).")
|
||||
return
|
||||
}
|
||||
mockResponseFound = true
|
||||
break
|
||||
}
|
||||
|
||||
if !mockResponseFound {
|
||||
// XCTFail("Missing mock response for: \(urlString)")
|
||||
testFileURL = providerUrl
|
||||
|
||||
} else if let testKeyAndFileName = testFiles.first(where: { urlString.contains($0.key) }) {
|
||||
testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testKeyAndFileName.value)
|
||||
|
||||
} else {
|
||||
// XCTFail("Missing mock response for: \(urlString)")
|
||||
print("***\nWARNING: \(self) missing mock response for:\n\(urlString)\n***")
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.success((response, nil)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: testFileURL)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.success((response, data)))
|
||||
}
|
||||
} catch {
|
||||
XCTFail("Unable to read file at \(testFileURL) because \(error).")
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send(request: URLRequest, method: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
|
|
@ -17,10 +17,10 @@ public protocol ArticleFetcher {
|
|||
func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock)
|
||||
}
|
||||
|
||||
extension Feed: ArticleFetcher {
|
||||
|
||||
extension WebFeed: ArticleFetcher {
|
||||
|
||||
public func fetchArticles() -> Set<Article> {
|
||||
return account?.fetchArticles(.feed(self)) ?? Set<Article>()
|
||||
return account?.fetchArticles(.webFeed(self)) ?? Set<Article>()
|
||||
}
|
||||
|
||||
public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
|
@ -29,7 +29,7 @@ extension Feed: ArticleFetcher {
|
|||
callback(Set<Article>())
|
||||
return
|
||||
}
|
||||
account.fetchArticlesAsync(.feed(self), callback)
|
||||
account.fetchArticlesAsync(.webFeed(self), callback)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticles() -> Set<Article> {
|
||||
|
@ -42,12 +42,12 @@ extension Feed: ArticleFetcher {
|
|||
callback(Set<Article>())
|
||||
return
|
||||
}
|
||||
account.fetchArticlesAsync(.feed(self)) { callback($0.unreadArticles()) }
|
||||
account.fetchArticlesAsync(.webFeed(self)) { callback($0.unreadArticles()) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Folder: ArticleFetcher {
|
||||
|
||||
|
||||
public func fetchArticles() -> Set<Article> {
|
||||
return fetchUnreadArticles()
|
||||
}
|
||||
|
|
|
@ -19,25 +19,25 @@ extension Notification.Name {
|
|||
public protocol Container: class {
|
||||
|
||||
var account: Account? { get }
|
||||
var topLevelFeeds: Set<Feed> { get set }
|
||||
var topLevelWebFeeds: Set<WebFeed> { get set }
|
||||
var folders: Set<Folder>? { get set }
|
||||
|
||||
func hasAtLeastOneFeed() -> Bool
|
||||
func hasAtLeastOneWebFeed() -> Bool
|
||||
func objectIsChild(_ object: AnyObject) -> Bool
|
||||
|
||||
func hasChildFolder(with: String) -> Bool
|
||||
func childFolder(with: String) -> Folder?
|
||||
|
||||
func removeFeed(_ feed: Feed)
|
||||
func addFeed(_ feed: Feed)
|
||||
func removeWebFeed(_ webFeed: WebFeed)
|
||||
func addWebFeed(_ webFeed: WebFeed)
|
||||
|
||||
//Recursive — checks subfolders
|
||||
func flattenedFeeds() -> Set<Feed>
|
||||
func has(_ feed: Feed) -> Bool
|
||||
func hasFeed(with feedID: String) -> Bool
|
||||
func hasFeed(withURL url: String) -> Bool
|
||||
func existingFeed(withFeedID: String) -> Feed?
|
||||
func existingFeed(withURL url: String) -> Feed?
|
||||
func flattenedWebFeeds() -> Set<WebFeed>
|
||||
func has(_ webFeed: WebFeed) -> Bool
|
||||
func hasWebFeed(with webFeedID: String) -> Bool
|
||||
func hasWebFeed(withURL url: String) -> Bool
|
||||
func existingWebFeed(withWebFeedID: String) -> WebFeed?
|
||||
func existingWebFeed(withURL url: String) -> WebFeed?
|
||||
func existingFolder(with name: String) -> Folder?
|
||||
func existingFolder(withID: Int) -> Folder?
|
||||
|
||||
|
@ -46,8 +46,8 @@ public protocol Container: class {
|
|||
|
||||
public extension Container {
|
||||
|
||||
func hasAtLeastOneFeed() -> Bool {
|
||||
return topLevelFeeds.count > 0
|
||||
func hasAtLeastOneWebFeed() -> Bool {
|
||||
return topLevelWebFeeds.count > 0
|
||||
}
|
||||
|
||||
func hasChildFolder(with name: String) -> Bool {
|
||||
|
@ -67,8 +67,8 @@ public extension Container {
|
|||
}
|
||||
|
||||
func objectIsChild(_ object: AnyObject) -> Bool {
|
||||
if let feed = object as? Feed {
|
||||
return topLevelFeeds.contains(feed)
|
||||
if let feed = object as? WebFeed {
|
||||
return topLevelWebFeeds.contains(feed)
|
||||
}
|
||||
if let folder = object as? Folder {
|
||||
return folders?.contains(folder) ?? false
|
||||
|
@ -76,40 +76,40 @@ public extension Container {
|
|||
return false
|
||||
}
|
||||
|
||||
func flattenedFeeds() -> Set<Feed> {
|
||||
var feeds = Set<Feed>()
|
||||
feeds.formUnion(topLevelFeeds)
|
||||
func flattenedWebFeeds() -> Set<WebFeed> {
|
||||
var feeds = Set<WebFeed>()
|
||||
feeds.formUnion(topLevelWebFeeds)
|
||||
if let folders = folders {
|
||||
for folder in folders {
|
||||
feeds.formUnion(folder.flattenedFeeds())
|
||||
feeds.formUnion(folder.flattenedWebFeeds())
|
||||
}
|
||||
}
|
||||
return feeds
|
||||
}
|
||||
|
||||
func hasFeed(with feedID: String) -> Bool {
|
||||
return existingFeed(withFeedID: feedID) != nil
|
||||
func hasWebFeed(with webFeedID: String) -> Bool {
|
||||
return existingWebFeed(withWebFeedID: webFeedID) != nil
|
||||
}
|
||||
|
||||
func hasFeed(withURL url: String) -> Bool {
|
||||
return existingFeed(withURL: url) != nil
|
||||
func hasWebFeed(withURL url: String) -> Bool {
|
||||
return existingWebFeed(withURL: url) != nil
|
||||
}
|
||||
|
||||
func has(_ feed: Feed) -> Bool {
|
||||
return flattenedFeeds().contains(feed)
|
||||
func has(_ webFeed: WebFeed) -> Bool {
|
||||
return flattenedWebFeeds().contains(webFeed)
|
||||
}
|
||||
|
||||
func existingFeed(withFeedID feedID: String) -> Feed? {
|
||||
for feed in flattenedFeeds() {
|
||||
if feed.feedID == feedID {
|
||||
func existingWebFeed(withWebFeedID webFeedID: String) -> WebFeed? {
|
||||
for feed in flattenedWebFeeds() {
|
||||
if feed.webFeedID == webFeedID {
|
||||
return feed
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func existingFeed(withURL url: String) -> Feed? {
|
||||
for feed in flattenedFeeds() {
|
||||
func existingWebFeed(withURL url: String) -> WebFeed? {
|
||||
for feed in flattenedWebFeeds() {
|
||||
if feed.url == url {
|
||||
return feed
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ public enum CredentialsType: String {
|
|||
case oauthRefreshToken = "oauthRefreshToken"
|
||||
}
|
||||
|
||||
public struct Credentials {
|
||||
public struct Credentials: Equatable {
|
||||
public let type: CredentialsType
|
||||
public let username: String
|
||||
public let secret: String
|
||||
|
|
|
@ -20,11 +20,12 @@ public struct CredentialsManager {
|
|||
}()
|
||||
|
||||
public static func storeCredentials(_ credentials: Credentials, server: String) throws {
|
||||
|
||||
|
||||
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: credentials.username,
|
||||
kSecAttrServer as String: server]
|
||||
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
||||
kSecAttrAccount as String: credentials.username,
|
||||
kSecAttrServer as String: server]
|
||||
|
||||
if credentials.type != .basic {
|
||||
query[kSecAttrSecurityDomain as String] = credentials.type.rawValue
|
||||
}
|
||||
|
@ -32,26 +33,25 @@ public struct CredentialsManager {
|
|||
if let securityGroup = keychainGroup {
|
||||
query[kSecAttrAccessGroup as String] = securityGroup
|
||||
}
|
||||
|
||||
|
||||
let secretData = credentials.secret.data(using: String.Encoding.utf8)!
|
||||
let attributes: [String: Any] = [kSecValueData as String: secretData]
|
||||
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
||||
|
||||
query[kSecValueData as String] = secretData
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
return
|
||||
case errSecItemNotFound:
|
||||
case errSecDuplicateItem:
|
||||
break
|
||||
default:
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
}
|
||||
|
||||
guard status == errSecItemNotFound else {
|
||||
return
|
||||
}
|
||||
var deleteQuery = query
|
||||
deleteQuery.removeValue(forKey: kSecAttrAccessible as String)
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
query[kSecValueData as String] = secretData
|
||||
|
||||
let addStatus = SecItemAdd(query as CFDictionary, nil)
|
||||
if addStatus != errSecSuccess {
|
||||
throw CredentialsError.unhandledError(status: status)
|
||||
|
|
|
@ -11,14 +11,14 @@ import Articles
|
|||
import RSParser
|
||||
|
||||
public extension Notification.Name {
|
||||
static let FeedSettingDidChange = Notification.Name(rawValue: "FeedSettingDidChangeNotification")
|
||||
static let WebFeedSettingDidChange = Notification.Name(rawValue: "FeedSettingDidChangeNotification")
|
||||
}
|
||||
|
||||
public extension Feed {
|
||||
public extension WebFeed {
|
||||
|
||||
static let FeedSettingUserInfoKey = "feedSetting"
|
||||
static let WebFeedSettingUserInfoKey = "feedSetting"
|
||||
|
||||
struct FeedSettingKey {
|
||||
struct WebFeedSettingKey {
|
||||
public static let homePageURL = "homePageURL"
|
||||
public static let iconURL = "iconURL"
|
||||
public static let faviconURL = "faviconURL"
|
||||
|
@ -30,7 +30,7 @@ public extension Feed {
|
|||
}
|
||||
}
|
||||
|
||||
extension Feed {
|
||||
extension WebFeed {
|
||||
|
||||
func takeSettings(from parsedFeed: ParsedFeed) {
|
||||
iconURL = parsedFeed.iconURL
|
||||
|
@ -40,9 +40,9 @@ extension Feed {
|
|||
authors = Author.authorsWithParsedAuthors(parsedFeed.authors)
|
||||
}
|
||||
|
||||
func postFeedSettingDidChangeNotification(_ codingKey: FeedMetadata.CodingKeys) {
|
||||
let userInfo = [Feed.FeedSettingUserInfoKey: codingKey.stringValue]
|
||||
NotificationCenter.default.post(name: .FeedSettingDidChange, object: self, userInfo: userInfo)
|
||||
func postFeedSettingDidChangeNotification(_ codingKey: WebFeedMetadata.CodingKeys) {
|
||||
let userInfo = [WebFeed.WebFeedSettingUserInfoKey: codingKey.stringValue]
|
||||
NotificationCenter.default.post(name: .WebFeedSettingDidChange, object: self, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,8 +56,8 @@ public extension Article {
|
|||
return manager.existingAccount(with: accountID)
|
||||
}
|
||||
|
||||
var feed: Feed? {
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
var webFeed: WebFeed? {
|
||||
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// DeepLinkProvider.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 10/3/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum DeepLinkKey: String {
|
||||
case accountID = "accountID"
|
||||
case accountName = "accountName"
|
||||
case feedID = "feedID"
|
||||
case articleID = "articleID"
|
||||
case folderName = "folderName"
|
||||
}
|
||||
|
||||
public protocol DeepLinkProvider {
|
||||
var deepLinkUserInfo: [AnyHashable : Any] { get }
|
||||
}
|
|
@ -1,277 +1,14 @@
|
|||
//
|
||||
// Feed.swift
|
||||
// NetNewsWire
|
||||
// Account
|
||||
//
|
||||
// Created by Brent Simmons on 7/1/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
// Created by Maurice Parker on 11/15/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSWeb
|
||||
import Articles
|
||||
|
||||
public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, DeepLinkProvider, Hashable {
|
||||
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
|
||||
|
||||
public weak var account: Account?
|
||||
public let url: String
|
||||
|
||||
public var feedID: String {
|
||||
get {
|
||||
return metadata.feedID
|
||||
}
|
||||
set {
|
||||
metadata.feedID = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var homePageURL: String? {
|
||||
get {
|
||||
return metadata.homePageURL
|
||||
}
|
||||
set {
|
||||
if let url = newValue {
|
||||
metadata.homePageURL = url.rs_normalizedURL()
|
||||
}
|
||||
else {
|
||||
metadata.homePageURL = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this is available only if the icon URL was available in the feed.
|
||||
// The icon URL is a JSON-Feed-only feature.
|
||||
// Otherwise we find an icon URL via other means, but we don’t store it
|
||||
// as part of feed metadata.
|
||||
public var iconURL: String? {
|
||||
get {
|
||||
return metadata.iconURL
|
||||
}
|
||||
set {
|
||||
metadata.iconURL = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this is available only if the favicon URL was available in the feed.
|
||||
// The favicon URL is a JSON-Feed-only feature.
|
||||
// Otherwise we find a favicon URL via other means, but we don’t store it
|
||||
// as part of feed metadata.
|
||||
public var faviconURL: String? {
|
||||
get {
|
||||
return metadata.faviconURL
|
||||
}
|
||||
set {
|
||||
metadata.faviconURL = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var name: String?
|
||||
|
||||
public var authors: Set<Author>? {
|
||||
get {
|
||||
if let authorsArray = metadata.authors {
|
||||
return Set(authorsArray)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
set {
|
||||
if let authorsSet = newValue {
|
||||
metadata.authors = Array(authorsSet)
|
||||
}
|
||||
else {
|
||||
metadata.authors = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var editedName: String? {
|
||||
// Don’t let editedName == ""
|
||||
get {
|
||||
guard let s = metadata.editedName, !s.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
set {
|
||||
if newValue != editedName {
|
||||
if let valueToSet = newValue, !valueToSet.isEmpty {
|
||||
metadata.editedName = valueToSet
|
||||
}
|
||||
else {
|
||||
metadata.editedName = nil
|
||||
}
|
||||
postDisplayNameDidChangeNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var conditionalGetInfo: HTTPConditionalGetInfo? {
|
||||
get {
|
||||
return metadata.conditionalGetInfo
|
||||
}
|
||||
set {
|
||||
metadata.conditionalGetInfo = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var contentHash: String? {
|
||||
get {
|
||||
return metadata.contentHash
|
||||
}
|
||||
set {
|
||||
metadata.contentHash = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var isNotifyAboutNewArticles: Bool? {
|
||||
get {
|
||||
return metadata.isNotifyAboutNewArticles
|
||||
}
|
||||
set {
|
||||
metadata.isNotifyAboutNewArticles = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var isArticleExtractorAlwaysOn: Bool? {
|
||||
get {
|
||||
return metadata.isArticleExtractorAlwaysOn
|
||||
}
|
||||
set {
|
||||
metadata.isArticleExtractorAlwaysOn = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var subscriptionID: String? {
|
||||
get {
|
||||
return metadata.subscriptionID
|
||||
}
|
||||
set {
|
||||
metadata.subscriptionID = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// Folder Name: Sync Service Relationship ID
|
||||
public var folderRelationship: [String: String]? {
|
||||
get {
|
||||
return metadata.folderRelationship
|
||||
}
|
||||
set {
|
||||
metadata.folderRelationship = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DisplayNameProvider
|
||||
|
||||
public var nameForDisplay: String {
|
||||
if let s = editedName, !s.isEmpty {
|
||||
return s
|
||||
}
|
||||
if let s = name, !s.isEmpty {
|
||||
return s
|
||||
}
|
||||
return NSLocalizedString("Untitled", comment: "Feed name")
|
||||
}
|
||||
|
||||
// MARK: - Renamable
|
||||
|
||||
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let account = account else { return }
|
||||
account.renameFeed(self, to: newName, completion: completion)
|
||||
}
|
||||
|
||||
// MARK: - PathIDUserInfoProvider
|
||||
public var deepLinkUserInfo: [AnyHashable : Any] {
|
||||
return [
|
||||
DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
|
||||
DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
|
||||
DeepLinkKey.feedID.rawValue: feedID
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - UnreadCountProvider
|
||||
|
||||
public var unreadCount: Int {
|
||||
get {
|
||||
return account?.unreadCount(for: self) ?? 0
|
||||
}
|
||||
set {
|
||||
if unreadCount == newValue {
|
||||
return
|
||||
}
|
||||
account?.setUnreadCount(newValue, for: self)
|
||||
postUnreadCountDidChangeNotification()
|
||||
}
|
||||
}
|
||||
|
||||
var metadata: FeedMetadata
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private let accountID: String // Used for hashing and equality; account may turn nil
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(account: Account, url: String, metadata: FeedMetadata) {
|
||||
self.account = account
|
||||
self.accountID = account.accountID
|
||||
self.url = url
|
||||
self.metadata = metadata
|
||||
}
|
||||
|
||||
// MARK: - Debug
|
||||
|
||||
public func debugDropConditionalGetInfo() {
|
||||
conditionalGetInfo = nil
|
||||
contentHash = nil
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(feedID)
|
||||
hasher.combine(accountID)
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
public class func ==(lhs: Feed, rhs: Feed) -> Bool {
|
||||
return lhs.feedID == rhs.feedID && lhs.accountID == rhs.accountID
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OPMLRepresentable
|
||||
|
||||
extension Feed: OPMLRepresentable {
|
||||
|
||||
public func OPMLString(indentLevel: Int, strictConformance: Bool) -> String {
|
||||
// https://github.com/brentsimmons/NetNewsWire/issues/527
|
||||
// Don’t use nameForDisplay because that can result in a feed name "Untitled" written to disk,
|
||||
// which NetNewsWire may take later to be the actual name.
|
||||
var nameToUse = editedName
|
||||
if nameToUse == nil {
|
||||
nameToUse = name
|
||||
}
|
||||
if nameToUse == nil {
|
||||
nameToUse = ""
|
||||
}
|
||||
let escapedName = nameToUse!.rs_stringByEscapingSpecialXMLCharacters()
|
||||
|
||||
var escapedHomePageURL = ""
|
||||
if let homePageURL = homePageURL {
|
||||
escapedHomePageURL = homePageURL.rs_stringByEscapingSpecialXMLCharacters()
|
||||
}
|
||||
let escapedFeedURL = url.rs_stringByEscapingSpecialXMLCharacters()
|
||||
|
||||
var s = "<outline text=\"\(escapedName)\" title=\"\(escapedName)\" description=\"\" type=\"rss\" version=\"RSS\" htmlUrl=\"\(escapedHomePageURL)\" xmlUrl=\"\(escapedFeedURL)\"/>\n"
|
||||
s = s.rs_string(byPrependingNumberOfTabs: indentLevel)
|
||||
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
extension Set where Element == Feed {
|
||||
|
||||
func feedIDs() -> Set<String> {
|
||||
return Set<String>(map { $0.feedID })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// ArticleFetcherType.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 11/13/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol FeedIdentifiable {
|
||||
var feedID: FeedIdentifier? { get }
|
||||
}
|
||||
|
||||
public enum FeedIdentifier: CustomStringConvertible {
|
||||
|
||||
case smartFeed(String) // String is a unique identifier
|
||||
case script(String) // String is a unique identifier
|
||||
case webFeed(String, String) // accountID, webFeedID
|
||||
case folder(String, String) // accountID, folderName
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .smartFeed(let id):
|
||||
return "smartFeed: \(id)"
|
||||
case .script(let id):
|
||||
return "script: \(id)"
|
||||
case .webFeed(let accountID, let webFeedID):
|
||||
return "feed: \(accountID)_\(webFeedID)"
|
||||
case .folder(let accountID, let folderName):
|
||||
return "folder: \(accountID)_\(folderName)"
|
||||
}
|
||||
}
|
||||
|
||||
public var userInfo: [AnyHashable: Any] {
|
||||
switch self {
|
||||
case .smartFeed(let id):
|
||||
return [
|
||||
"type": "smartFeed",
|
||||
"id": id
|
||||
]
|
||||
case .script(let id):
|
||||
return [
|
||||
"type": "script",
|
||||
"id": id
|
||||
]
|
||||
case .webFeed(let accountID, let webFeedID):
|
||||
return [
|
||||
"type": "feed",
|
||||
"accountID": accountID,
|
||||
"webFeedID": webFeedID
|
||||
]
|
||||
case .folder(let accountID, let folderName):
|
||||
return [
|
||||
"type": "folder",
|
||||
"accountID": accountID,
|
||||
"folderName": folderName
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public init?(userInfo: [AnyHashable: Any]) {
|
||||
guard let type = userInfo["type"] as? String else { return nil }
|
||||
|
||||
switch type {
|
||||
case "smartFeed":
|
||||
guard let id = userInfo["id"] as? String else { return nil }
|
||||
self = FeedIdentifier.smartFeed(id)
|
||||
case "script":
|
||||
guard let id = userInfo["id"] as? String else { return nil }
|
||||
self = FeedIdentifier.script(id)
|
||||
case "feed":
|
||||
guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil }
|
||||
self = FeedIdentifier.webFeed(accountID, webFeedID)
|
||||
case "folder":
|
||||
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
|
||||
self = FeedIdentifier.folder(accountID, folderName)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -26,7 +26,6 @@ final class FeedbinAPICaller: NSObject {
|
|||
static let subscriptions = "subscriptions"
|
||||
static let tags = "tags"
|
||||
static let taggings = "taggings"
|
||||
static let icons = "icons"
|
||||
static let unreadEntries = "unreadEntries"
|
||||
static let starredEntries = "starredEntries"
|
||||
}
|
||||
|
@ -42,6 +41,10 @@ final class FeedbinAPICaller: NSObject {
|
|||
self.transport = transport
|
||||
}
|
||||
|
||||
func cancelAll() {
|
||||
transport.cancelAll()
|
||||
}
|
||||
|
||||
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("authentication.json")
|
||||
|
@ -145,9 +148,11 @@ final class FeedbinAPICaller: NSObject {
|
|||
|
||||
func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
||||
var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("subscriptions.json"), resolvingAgainstBaseURL: false)!
|
||||
callComponents.queryItems = [URLQueryItem(name: "mode", value: "extended")]
|
||||
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
let request = URLRequest(url: callComponents.url!, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [FeedbinSubscription].self) { result in
|
||||
|
||||
|
@ -165,8 +170,10 @@ final class FeedbinAPICaller: NSObject {
|
|||
|
||||
func createSubscription(url: String, completion: @escaping (Result<CreateSubscriptionResult, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("subscriptions.json"), resolvingAgainstBaseURL: false)!
|
||||
callComponents.queryItems = [URLQueryItem(name: "mode", value: "extended")]
|
||||
|
||||
var request = URLRequest(url: callComponents.url!, credentials: credentials)
|
||||
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
|
||||
let payload: Data
|
||||
|
@ -268,7 +275,7 @@ final class FeedbinAPICaller: NSObject {
|
|||
|
||||
}
|
||||
|
||||
func createTagging(feedID: Int, name: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
func createTagging(webFeedID: Int, name: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("taggings.json")
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
@ -276,7 +283,7 @@ final class FeedbinAPICaller: NSObject {
|
|||
|
||||
let payload: Data
|
||||
do {
|
||||
payload = try JSONEncoder().encode(FeedbinCreateTagging(feedID: feedID, name: name))
|
||||
payload = try JSONEncoder().encode(FeedbinCreateTagging(feedID: webFeedID, name: name))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
|
@ -309,26 +316,6 @@ final class FeedbinAPICaller: NSObject {
|
|||
transport.send(request: request, method: HTTPMethod.delete, completion: completion)
|
||||
}
|
||||
|
||||
func retrieveIcons(completion: @escaping (Result<[FeedbinIcon]?, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("icons.json")
|
||||
let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons]
|
||||
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
|
||||
|
||||
transport.send(request: request, resultType: [FeedbinIcon].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (response, icons)):
|
||||
self.storeConditionalGet(key: ConditionalGetKeys.icons, headers: response.allHeaderFields)
|
||||
completion(.success(icons))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) {
|
||||
|
||||
guard !articleIDs.isEmpty else {
|
||||
|
@ -390,7 +377,7 @@ final class FeedbinAPICaller: NSObject {
|
|||
|
||||
}
|
||||
|
||||
func retrieveEntries(completion: @escaping (Result<([FeedbinEntry]?, String?, Int?), Error>) -> Void) {
|
||||
func retrieveEntries(completion: @escaping (Result<([FeedbinEntry]?, String?, Date?, Int?), Error>) -> Void) {
|
||||
|
||||
let since: Date = {
|
||||
if let lastArticleFetch = accountMetadata?.lastArticleFetch {
|
||||
|
@ -416,14 +403,12 @@ final class FeedbinAPICaller: NSObject {
|
|||
case .success(let (response, entries)):
|
||||
|
||||
let dateInfo = HTTPDateInfo(urlResponse: response)
|
||||
self.accountMetadata?.lastArticleFetch = dateInfo?.date
|
||||
|
||||
let pagingInfo = HTTPLinkPagingInfo(urlResponse: response)
|
||||
let lastPageNumber = self.extractPageNumber(link: pagingInfo.lastPage)
|
||||
completion(.success((entries, pagingInfo.nextPage, lastPageNumber)))
|
||||
completion(.success((entries, pagingInfo.nextPage, dateInfo?.date, lastPageNumber)))
|
||||
|
||||
case .failure(let error):
|
||||
self.accountMetadata?.lastArticleFetch = nil
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
|
@ -449,7 +434,6 @@ final class FeedbinAPICaller: NSObject {
|
|||
completion(.success((entries, pagingInfo.nextPage)))
|
||||
|
||||
case .failure(let error):
|
||||
self.accountMetadata?.lastArticleFetch = nil
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import os.log
|
|||
|
||||
public enum FeedbinAccountDelegateError: String, Error {
|
||||
case invalidParameter = "There was an invalid parameter passed."
|
||||
case unknown = "An unknown error occurred."
|
||||
}
|
||||
|
||||
final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
@ -72,24 +73,27 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
func cancelAll(for account: Account) {
|
||||
caller.cancelAll()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(6)
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||
|
||||
refreshAccount(account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
|
||||
self.sendArticleStatus(for: account) {
|
||||
self.refreshArticleStatus(for: account) {
|
||||
self.refreshArticles(account) {
|
||||
self.refreshMissingArticles(account) {
|
||||
self.refreshProgress.clear()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
self.refreshArticlesAndStatuses(account) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,8 +110,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
|
||||
os_log(.debug, log: log, "Sending article statuses...")
|
||||
|
||||
|
@ -118,41 +121,58 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false }
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
sendArticleStatuses(createUnreadStatuses, apiCall: caller.createUnreadEntries) {
|
||||
sendArticleStatuses(createUnreadStatuses, apiCall: caller.createUnreadEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
sendArticleStatuses(deleteUnreadStatuses, apiCall: caller.deleteUnreadEntries) {
|
||||
sendArticleStatuses(deleteUnreadStatuses, apiCall: caller.deleteUnreadEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
sendArticleStatuses(createStarredStatuses, apiCall: caller.createStarredEntries) {
|
||||
sendArticleStatuses(createStarredStatuses, apiCall: caller.createStarredEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
sendArticleStatuses(deleteStarredStatuses, apiCall: caller.deleteStarredEntries) {
|
||||
sendArticleStatuses(deleteStarredStatuses, apiCall: caller.deleteStarredEntries) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
completion()
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
|
||||
os_log(.debug, log: log, "Refreshing article statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
caller.retrieveUnreadEntries() { result in
|
||||
switch result {
|
||||
|
@ -160,6 +180,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
self.syncArticleReadState(account: account, articleIDs: articleIDs)
|
||||
group.leave()
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
|
@ -173,6 +194,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
self.syncArticleStarredState(account: account, articleIDs: articleIDs)
|
||||
group.leave()
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
|
@ -181,13 +203,16 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||
completion()
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
|
||||
var fileData: Data?
|
||||
|
||||
|
@ -234,7 +259,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
|
@ -243,9 +267,8 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
|
||||
guard folder.hasAtLeastOneFeed() else {
|
||||
guard folder.hasAtLeastOneWebFeed() else {
|
||||
folder.name = name
|
||||
return
|
||||
}
|
||||
|
@ -271,10 +294,9 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
|
||||
// Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system
|
||||
guard folder.hasAtLeastOneFeed() else {
|
||||
guard folder.hasAtLeastOneWebFeed() else {
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
return
|
||||
|
@ -282,7 +304,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
let group = DispatchGroup()
|
||||
|
||||
for feed in folder.topLevelFeeds {
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
|
||||
if feed.folderRelationship?.count ?? 0 > 1 {
|
||||
|
||||
|
@ -314,7 +336,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearFeedMetadata(feed)
|
||||
account.clearWebFeedMetadata(feed)
|
||||
}
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
|
||||
|
@ -334,8 +356,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
caller.createSubscription(url: url) { result in
|
||||
|
@ -367,8 +388,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
|
@ -395,7 +415,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if feed.folderRelationship?.count ?? 0 > 1 {
|
||||
deleteTagging(for: account, with: feed, from: container, completion: completion)
|
||||
} else {
|
||||
|
@ -403,15 +423,14 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if from is Account {
|
||||
addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||
} else {
|
||||
deleteTagging(for: account, with: feed, from: from) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
self.addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
@ -419,19 +438,18 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedID = Int(feed.feedID) {
|
||||
if let folder = container as? Folder, let webFeedID = Int(feed.webFeedID) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in
|
||||
caller.createTagging(webFeedID: webFeedID, name: folder.name ?? "") { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let taggingID):
|
||||
DispatchQueue.main.async {
|
||||
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID))
|
||||
account.removeFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
account.removeWebFeed(feed)
|
||||
folder.addWebFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
|
@ -452,11 +470,10 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||
account.addWebFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
@ -465,7 +482,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
@ -478,16 +495,15 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
for feed in folder.topLevelFeeds {
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
|
||||
folder.topLevelFeeds.remove(feed)
|
||||
folder.topLevelWebFeeds.remove(feed)
|
||||
|
||||
group.enter()
|
||||
restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
|
@ -507,7 +523,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
retrieveCredentialsIfNecessary(account)
|
||||
|
||||
let syncStatuses = articles.map { article in
|
||||
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag)
|
||||
|
@ -515,7 +530,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
database.insertStatuses(syncStatuses)
|
||||
|
||||
if database.selectPendingCount() > 100 {
|
||||
sendArticleStatus(for: account) {}
|
||||
sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
|
||||
return account.update(articles, statusKey: statusKey, flag: flag)
|
||||
|
@ -526,6 +541,9 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
credentials = try? account.retrieveCredentials(type: .basic)
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
|
||||
let caller = FeedbinAPICaller(transport: transport)
|
||||
|
@ -598,26 +616,14 @@ private extension FeedbinAccountDelegate {
|
|||
switch result {
|
||||
case .success(let taggings):
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.retrieveIcons { result in
|
||||
switch result {
|
||||
case .success(let icons):
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
self.syncFolders(account, tags)
|
||||
self.syncFeeds(account, subscriptions)
|
||||
self.syncFeedFolderRelationship(account, taggings)
|
||||
self.syncFavicons(account, icons)
|
||||
}
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
self.syncFolders(account, tags)
|
||||
self.syncFeeds(account, subscriptions)
|
||||
self.syncFeedFolderRelationship(account, taggings)
|
||||
}
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
|
@ -639,6 +645,49 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func refreshArticlesAndStatuses(_ account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshArticles(account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshMissingArticles(account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function can be deleted if Feedbin updates their taggings.json service to
|
||||
// show a change when a tag is renamed.
|
||||
func forceExpireFolderFeedRelationship(_ account: Account, _ tags: [FeedbinTag]?) {
|
||||
|
@ -675,8 +724,8 @@ private extension FeedbinAccountDelegate {
|
|||
if let folders = account.folders {
|
||||
folders.forEach { folder in
|
||||
if !tagNames.contains(folder.name ?? "") {
|
||||
for feed in folder.topLevelFeeds {
|
||||
account.addFeed(feed)
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
account.addWebFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
}
|
||||
account.removeFolder(folder)
|
||||
|
@ -713,17 +762,17 @@ private extension FeedbinAccountDelegate {
|
|||
// Remove any feeds that are no longer in the subscriptions
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !subFeedIds.contains(feed.feedID) {
|
||||
folder.removeFeed(feed)
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
if !subFeedIds.contains(feed.webFeedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for feed in account.topLevelFeeds {
|
||||
if !subFeedIds.contains(feed.feedID) {
|
||||
account.removeFeed(feed)
|
||||
for feed in account.topLevelWebFeeds {
|
||||
if !subFeedIds.contains(feed.webFeedID) {
|
||||
account.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -733,12 +782,14 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
let subFeedId = String(subscription.feedID)
|
||||
|
||||
if let feed = account.existingFeed(withFeedID: subFeedId) {
|
||||
if let feed = account.existingWebFeed(withWebFeedID: subFeedId) {
|
||||
feed.name = subscription.name
|
||||
// If the name has been changed on the server remove the locally edited name
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.homePageURL
|
||||
feed.subscriptionID = String(subscription.subscriptionID)
|
||||
feed.faviconURL = subscription.jsonFeed?.favicon
|
||||
feed.iconURL = subscription.jsonFeed?.icon
|
||||
}
|
||||
else {
|
||||
subscriptionsToAdd.insert(subscription)
|
||||
|
@ -747,9 +798,9 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
// Actually add subscriptions all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||
subscriptionsToAdd.forEach { subscription in
|
||||
let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: String(subscription.feedID), homePageURL: subscription.homePageURL)
|
||||
let feed = account.createWebFeed(with: subscription.name, url: subscription.url, webFeedID: String(subscription.feedID), homePageURL: subscription.homePageURL)
|
||||
feed.subscriptionID = String(subscription.subscriptionID)
|
||||
account.addFeed(feed)
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -788,25 +839,25 @@ private extension FeedbinAccountDelegate {
|
|||
let taggingFeedIDs = groupedTaggings.map { String($0.feedID) }
|
||||
|
||||
// Move any feeds not in the folder to the account
|
||||
for feed in folder.topLevelFeeds {
|
||||
if !taggingFeedIDs.contains(feed.feedID) {
|
||||
folder.removeFeed(feed)
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
if !taggingFeedIDs.contains(feed.webFeedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
account.addFeed(feed)
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any feeds not in the folder
|
||||
let folderFeedIds = folder.topLevelFeeds.map { $0.feedID }
|
||||
let folderFeedIds = folder.topLevelWebFeeds.map { $0.webFeedID }
|
||||
|
||||
for tagging in groupedTaggings {
|
||||
let taggingFeedID = String(tagging.feedID)
|
||||
if !folderFeedIds.contains(taggingFeedID) {
|
||||
guard let feed = account.existingFeed(withFeedID: taggingFeedID) else {
|
||||
guard let feed = account.existingWebFeed(withWebFeedID: taggingFeedID) else {
|
||||
continue
|
||||
}
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: String(tagging.taggingID))
|
||||
folder.addFeed(feed)
|
||||
folder.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -815,42 +866,24 @@ private extension FeedbinAccountDelegate {
|
|||
let taggedFeedIDs = Set(taggings.map { String($0.feedID) })
|
||||
|
||||
// Remove all feeds from the account container that have a tag
|
||||
for feed in account.topLevelFeeds {
|
||||
if taggedFeedIDs.contains(feed.feedID) {
|
||||
account.removeFeed(feed)
|
||||
for feed in account.topLevelWebFeeds {
|
||||
if taggedFeedIDs.contains(feed.webFeedID) {
|
||||
account.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncFavicons(_ account: Account, _ icons: [FeedbinIcon]?) {
|
||||
|
||||
guard let icons = icons else { return }
|
||||
|
||||
os_log(.debug, log: log, "Syncing favicons with %ld icons.", icons.count)
|
||||
|
||||
let iconDict = Dictionary(uniqueKeysWithValues: icons.map { ($0.host, $0.url) } )
|
||||
|
||||
for feed in account.flattenedFeeds() {
|
||||
for (key, value) in iconDict {
|
||||
if feed.homePageURL?.contains(key) ?? false {
|
||||
feed.faviconURL = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func sendArticleStatuses(_ statuses: [SyncStatus],
|
||||
apiCall: ([Int], @escaping (Result<Void, Error>) -> Void) -> Void,
|
||||
completion: @escaping (() -> Void)) {
|
||||
completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
|
||||
guard !statuses.isEmpty else {
|
||||
completion()
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let articleIDs = statuses.compactMap { Int($0.articleID) }
|
||||
let articleIDGroups = articleIDs.chunked(into: 1000)
|
||||
|
@ -863,6 +896,7 @@ private extension FeedbinAccountDelegate {
|
|||
self.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } )
|
||||
group.leave()
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Article status sync call failed: %@.", error.localizedDescription)
|
||||
self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } )
|
||||
group.leave()
|
||||
|
@ -872,13 +906,17 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renameFolderRelationship(for account: Account, fromName: String, toName: String) {
|
||||
for feed in account.flattenedFeeds() {
|
||||
for feed in account.flattenedWebFeeds() {
|
||||
if var folderRelationship = feed.folderRelationship {
|
||||
let relationship = folderRelationship[fromName]
|
||||
folderRelationship[fromName] = nil
|
||||
|
@ -888,14 +926,14 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
|
||||
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) {
|
||||
if var folderRelationship = feed.folderRelationship {
|
||||
folderRelationship[folderName] = nil
|
||||
feed.folderRelationship = folderRelationship
|
||||
}
|
||||
}
|
||||
|
||||
func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) {
|
||||
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) {
|
||||
if var folderRelationship = feed.folderRelationship {
|
||||
folderRelationship[folderName] = id
|
||||
feed.folderRelationship = folderRelationship
|
||||
|
@ -904,7 +942,7 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
let feedSpecifiers: [FeedSpecifier] = choices.map { choice in
|
||||
let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink
|
||||
|
@ -914,7 +952,7 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) {
|
||||
if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first {
|
||||
createFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion)
|
||||
createWebFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
|
@ -928,19 +966,20 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
let feed = account.createWebFeed(with: sub.name, url: sub.url, webFeedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
feed.subscriptionID = String(sub.subscriptionID)
|
||||
feed.iconURL = sub.jsonFeed?.icon
|
||||
feed.faviconURL = sub.jsonFeed?.favicon
|
||||
|
||||
account.addFeed(feed, to: container) { result in
|
||||
account.addWebFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameFeed(feed, to: name) { result in
|
||||
account.renameWebFeed(feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
|
@ -960,28 +999,51 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func initialFeedDownload( account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
// refreshArticles is being reused and will clear one of the tasks for us
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(4)
|
||||
|
||||
// Download the initial articles
|
||||
self.caller.retrieveEntries(feedID: feed.feedID) { result in
|
||||
self.caller.retrieveEntries(feedID: feed.webFeedID) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success(let (entries, page)):
|
||||
|
||||
self.processEntries(account: account, entries: entries) {
|
||||
self.refreshArticleStatus(for: account) {
|
||||
self.refreshArticles(account, page: page) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) {
|
||||
self.refreshProgress.completeTask()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshMissingArticles(account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -994,14 +1056,14 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
}
|
||||
|
||||
func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) {
|
||||
func refreshArticles(_ account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
|
||||
os_log(.debug, log: log, "Refreshing articles...")
|
||||
|
||||
caller.retrieveEntries() { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (entries, page, lastPageNumber)):
|
||||
case .success(let (entries, page, updateFetchDate, lastPageNumber)):
|
||||
|
||||
if let last = lastPageNumber {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(last - 1)
|
||||
|
@ -1010,25 +1072,30 @@ private extension FeedbinAccountDelegate {
|
|||
self.processEntries(account: account, entries: entries) {
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshArticles(account, page: page) {
|
||||
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
|
||||
os_log(.debug, log: self.log, "Done refreshing articles.")
|
||||
completion()
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Refresh articles failed: %@.", error.localizedDescription)
|
||||
completion()
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func refreshMissingArticles(_ account: Account, completion: @escaping (() -> Void)) {
|
||||
func refreshMissingArticles(_ account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing missing articles...")
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles()
|
||||
let articleIDs = Array(fetchedArticleIDs)
|
||||
|
@ -1046,6 +1113,7 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
|
@ -1055,13 +1123,20 @@ private extension FeedbinAccountDelegate {
|
|||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing articles.")
|
||||
completion()
|
||||
if errorOccurred {
|
||||
completion(.failure(FeedbinAccountDelegateError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) {
|
||||
func refreshArticles(_ account: Account, page: String?, updateFetchDate: Date?, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard let page = page else {
|
||||
completion()
|
||||
if let lastArticleFetch = updateFetchDate {
|
||||
self.accountMetadata?.lastArticleFetch = lastArticleFetch
|
||||
}
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1072,20 +1147,19 @@ private extension FeedbinAccountDelegate {
|
|||
|
||||
self.processEntries(account: account, entries: entries) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshArticles(account, page: nextPage, completion: completion)
|
||||
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Refresh articles for additional pages failed: %@.", error.localizedDescription)
|
||||
completion()
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processEntries(account: Account, entries: [FeedbinEntry]?, completion: @escaping (() -> Void)) {
|
||||
let parsedItems = mapEntriesToParsedItems(entries: entries)
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true, completion: completion)
|
||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true, completion: completion)
|
||||
}
|
||||
|
||||
func mapEntriesToParsedItems(entries: [FeedbinEntry]?) -> Set<ParsedItem> {
|
||||
|
@ -1161,7 +1235,7 @@ private extension FeedbinAccountDelegate {
|
|||
account.ensureStatuses(missingUnstarredArticleIDs, true, .starred, false)
|
||||
}
|
||||
|
||||
func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func deleteTagging(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
@ -1171,7 +1245,7 @@ private extension FeedbinAccountDelegate {
|
|||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
folder.removeFeed(feed)
|
||||
folder.removeWebFeed(feed)
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
@ -1184,14 +1258,14 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
account.removeFeed(feed)
|
||||
account.removeWebFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func deleteSubscription(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func deleteSubscription(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
|
@ -1205,11 +1279,11 @@ private extension FeedbinAccountDelegate {
|
|||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearFeedMetadata(feed)
|
||||
account.removeFeed(feed)
|
||||
account.clearWebFeedMetadata(feed)
|
||||
account.removeWebFeed(feed)
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
folder.removeFeed(feed)
|
||||
folder.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
completion(.success(()))
|
||||
|
@ -1223,11 +1297,5 @@ private extension FeedbinAccountDelegate {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveCredentialsIfNecessary(_ account: Account) {
|
||||
if credentials == nil {
|
||||
credentials = try? account.retrieveCredentials(type: .basic)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// FeedbinIcon.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 5/6/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedbinIcon: Codable {
|
||||
|
||||
let host: String
|
||||
let url: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case host
|
||||
case url
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@ struct FeedbinSubscription: Hashable, Codable {
|
|||
let name: String?
|
||||
let url: String
|
||||
let homePageURL: String?
|
||||
let jsonFeed: FeedbinSubscriptionJSONFeed?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case subscriptionID = "id"
|
||||
|
@ -24,11 +25,26 @@ struct FeedbinSubscription: Hashable, Codable {
|
|||
case name = "title"
|
||||
case url = "feed_url"
|
||||
case homePageURL = "site_url"
|
||||
case jsonFeed = "json_feed"
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(subscriptionID)
|
||||
}
|
||||
|
||||
static func == (lhs: FeedbinSubscription, rhs: FeedbinSubscription) -> Bool {
|
||||
return lhs.subscriptionID == rhs.subscriptionID
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct FeedbinSubscriptionJSONFeed: Codable {
|
||||
let favicon: String?
|
||||
let icon: String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case favicon = "favicon"
|
||||
case icon = "icon"
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedbinCreateSubscription: Codable {
|
||||
|
|
|
@ -15,15 +15,6 @@ final class FeedlyAPICaller {
|
|||
case sandbox
|
||||
case cloud
|
||||
|
||||
static var `default`: API {
|
||||
// https://developer.feedly.com/v3/developer/
|
||||
if let token = ProcessInfo.processInfo.environment["FEEDLY_DEV_ACCESS_TOKEN"], !token.isEmpty {
|
||||
return .cloud
|
||||
}
|
||||
|
||||
return .sandbox
|
||||
}
|
||||
|
||||
var baseUrlComponents: URLComponents {
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
|
@ -37,6 +28,15 @@ final class FeedlyAPICaller {
|
|||
}
|
||||
return components
|
||||
}
|
||||
|
||||
var oauthAuthorizationClient: OAuthAuthorizationClient {
|
||||
switch self {
|
||||
case .sandbox:
|
||||
return .feedlySandboxClient
|
||||
case .cloud:
|
||||
return .feedlyCloudClient
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let transport: Transport
|
||||
|
@ -53,169 +53,8 @@ final class FeedlyAPICaller {
|
|||
return baseUrlComponents.host
|
||||
}
|
||||
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/collections"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
// URLSession.shared.dataTask(with: request) { (data, response, error) in
|
||||
// print(String(data: data!, encoding: .utf8))
|
||||
// }.resume()
|
||||
//
|
||||
transport.send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStream(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/contents"
|
||||
|
||||
var queryItems = [URLQueryItem]()
|
||||
|
||||
if let date = newerThan {
|
||||
let value = String(Int(date.timeIntervalSince1970 * 1000))
|
||||
let queryItem = URLQueryItem(name: "newerThan", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let flag = unreadOnly {
|
||||
let value = flag ? "true" : "false"
|
||||
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let value = continuation, !value.isEmpty {
|
||||
let queryItem = URLQueryItem(name: "continuation", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
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: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MarkAction {
|
||||
case read
|
||||
case unread
|
||||
case saved
|
||||
case unsaved
|
||||
|
||||
var actionValue: String {
|
||||
switch self {
|
||||
case .read:
|
||||
return "markAsRead"
|
||||
case .unread:
|
||||
return "keepUnread"
|
||||
case .saved:
|
||||
return "markAsSaved"
|
||||
case .unsaved:
|
||||
return "markAsUnsaved"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MarkerEntriesBody: Encodable {
|
||||
let type = "entries"
|
||||
var action: String
|
||||
var entryIds: [String]
|
||||
}
|
||||
|
||||
func mark(_ articleIds: Set<String>, as action: MarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/markers"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
do {
|
||||
let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIds))
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(body)
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
transport.send(request: request, resultType: String.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))
|
||||
}
|
||||
}
|
||||
func cancelAll() {
|
||||
transport.cancelAll()
|
||||
}
|
||||
|
||||
func importOpml(_ opmlData: Data, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
|
@ -482,13 +321,14 @@ final class FeedlyAPICaller {
|
|||
|
||||
extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting {
|
||||
|
||||
static func authorizationCodeUrlRequest(for request: OAuthAuthorizationRequest) -> URLRequest {
|
||||
let api = API.default
|
||||
var components = api.baseUrlComponents
|
||||
static func authorizationCodeUrlRequest(for request: OAuthAuthorizationRequest, baseUrlComponents: URLComponents) -> URLRequest {
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/auth/auth"
|
||||
components.queryItems = request.queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
assert(components.scheme != nil)
|
||||
assert(components.host != nil)
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
|
@ -538,3 +378,341 @@ extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting {
|
||||
|
||||
func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completionHandler: @escaping (Result<FeedlyOAuthAccessTokenResponse, Error>) -> ()) {
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/auth/token"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
request.httpBody = try encoder.encode(refreshRequest)
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
transport.send(request: request, resultType: AccessTokenResponse.self, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, tokenResponse)):
|
||||
if let response = tokenResponse {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetCollectionsService {
|
||||
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/collections"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
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: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetStreamContentsService {
|
||||
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/contents"
|
||||
|
||||
var queryItems = [URLQueryItem]()
|
||||
|
||||
if let date = newerThan {
|
||||
let value = String(Int(date.timeIntervalSince1970 * 1000))
|
||||
let queryItem = URLQueryItem(name: "newerThan", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let flag = unreadOnly {
|
||||
let value = flag ? "true" : "false"
|
||||
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let value = continuation, !value.isEmpty {
|
||||
let queryItem = URLQueryItem(name: "continuation", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
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: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetStreamIdsService {
|
||||
|
||||
func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/ids"
|
||||
|
||||
var queryItems = [URLQueryItem]()
|
||||
|
||||
if let date = newerThan {
|
||||
let value = String(Int(date.timeIntervalSince1970 * 1000))
|
||||
let queryItem = URLQueryItem(name: "newerThan", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let flag = unreadOnly {
|
||||
let value = flag ? "true" : "false"
|
||||
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let value = continuation, !value.isEmpty {
|
||||
let queryItem = URLQueryItem(name: "continuation", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
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: FeedlyStreamIds.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetEntriesService {
|
||||
|
||||
func getEntries(for ids: Set<String>, completionHandler: @escaping (Result<[FeedlyEntry], Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/entries/.mget"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
do {
|
||||
let body = Array(ids)
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(body)
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
request.httpMethod = "POST"
|
||||
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: [FeedlyEntry].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, entries)):
|
||||
if let response = entries {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyMarkArticlesService {
|
||||
|
||||
private struct MarkerEntriesBody: Encodable {
|
||||
let type = "entries"
|
||||
var action: String
|
||||
var entryIds: [String]
|
||||
}
|
||||
|
||||
func mark(_ articleIds: Set<String>, as action: FeedlyMarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/markers"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
do {
|
||||
let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIds))
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(body)
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
transport.send(request: request, resultType: String.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: FeedlyLogoutService {
|
||||
|
||||
func logout(completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/auth/logout"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
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: String.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,19 +27,22 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
|
|||
|
||||
private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions"
|
||||
|
||||
static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest {
|
||||
static func oauthAuthorizationCodeGrantRequest() -> URLRequest {
|
||||
let client = environment.oauthAuthorizationClient
|
||||
let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id,
|
||||
redirectUri: client.redirectUri,
|
||||
scope: oauthAuthorizationGrantScope,
|
||||
state: client.state)
|
||||
return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest)
|
||||
let baseURLComponents = environment.baseUrlComponents
|
||||
return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents)
|
||||
}
|
||||
|
||||
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
|
||||
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
|
||||
let client = environment.oauthAuthorizationClient
|
||||
let request = OAuthAccessTokenRequest(authorizationResponse: response,
|
||||
scope: oauthAuthorizationGrantScope,
|
||||
client: client)
|
||||
let caller = FeedlyAPICaller(transport: transport, api: .default)
|
||||
let caller = FeedlyAPICaller(transport: transport, api: environment)
|
||||
caller.requestAccessToken(request) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
|
@ -62,3 +65,30 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAccountDelegate: OAuthAccessTokenRefreshing {
|
||||
func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completionHandler: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
|
||||
let request = OAuthRefreshAccessTokenRequest(refreshToken: refreshToken, scope: nil, client: client)
|
||||
|
||||
caller.refreshAccessToken(request) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken)
|
||||
|
||||
let refreshToken: Credentials? = {
|
||||
guard let token = response.refreshToken else {
|
||||
return nil
|
||||
}
|
||||
return Credentials(type: .oauthRefreshToken, username: response.id, secret: token)
|
||||
}()
|
||||
|
||||
let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken)
|
||||
|
||||
completionHandler(.success(grant))
|
||||
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,14 @@ import SyncDatabase
|
|||
import os.log
|
||||
|
||||
final class FeedlyAccountDelegate: AccountDelegate {
|
||||
|
||||
/// Feedly has a sandbox API and a production API.
|
||||
/// This property is referred to when clients need to know which environment it should be pointing to.
|
||||
/// The value of this proptery must match any `OAuthAuthorizationClient` used.
|
||||
/// Currently this is always returning the cloud API, but we are leaving it stubbed out for now.
|
||||
static var environment: FeedlyAPICaller.API {
|
||||
return .cloud
|
||||
}
|
||||
|
||||
// TODO: Kiel, if you decide not to support OPML import you will have to disallow it in the behaviors
|
||||
// See https://developer.feedly.com/v3/opml/
|
||||
|
@ -38,18 +46,30 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
let oauthAuthorizationClient: OAuthAuthorizationClient
|
||||
|
||||
var accountMetadata: AccountMetadata?
|
||||
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
private let caller: FeedlyAPICaller
|
||||
internal let caller: FeedlyAPICaller
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedly")
|
||||
private let database: SyncDatabase
|
||||
|
||||
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API = .default) {
|
||||
private weak var currentSyncAllOperation: FeedlySyncAllOperation?
|
||||
private let operationQueue: OperationQueue
|
||||
|
||||
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API) {
|
||||
self.operationQueue = OperationQueue()
|
||||
// Many operations have their own operation queues, such as the sync all operation.
|
||||
// Making this a serial queue at this higher level of abstraction means we can ensure,
|
||||
// for example, a `FeedlyRefreshAccessTokenOperation` occurs before a `FeedlySyncAllOperation`,
|
||||
// improving our ability to debug, reason about and predict the behaviour of the code.
|
||||
self.operationQueue.maxConcurrentOperationCount = 1
|
||||
|
||||
if let transport = transport {
|
||||
caller = FeedlyAPICaller(transport: transport, api: api)
|
||||
self.caller = FeedlyAPICaller(transport: transport, api: api)
|
||||
|
||||
} else {
|
||||
|
||||
|
@ -67,38 +87,66 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
let session = URLSession(configuration: sessionConfiguration)
|
||||
caller = FeedlyAPICaller(transport: session, api: api)
|
||||
self.caller = FeedlyAPICaller(transport: session, api: api)
|
||||
}
|
||||
|
||||
|
||||
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
|
||||
self.database = SyncDatabase(databaseFilePath: databaseFilePath)
|
||||
self.oauthAuthorizationClient = api.oauthAuthorizationClient
|
||||
}
|
||||
|
||||
// MARK: Account API
|
||||
|
||||
private var syncStrategy: FeedlySyncStrategy?
|
||||
func cancelAll(for account: Account) {
|
||||
operationQueue.cancelAllOperations()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let date = Date()
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
guard currentSyncAllOperation == nil else {
|
||||
os_log(.debug, log: log, "Ignoring refreshAll: Feedly sync already in progress.")
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
guard let credentials = credentials else {
|
||||
os_log(.debug, log: log, "Ignoring refreshAll: Feedly account has no credentials.")
|
||||
completion(.failure(FeedlyAccountDelegateError.notLoggedIn))
|
||||
return
|
||||
}
|
||||
|
||||
let log = self.log
|
||||
let progress = refreshProgress
|
||||
progress.addToNumberOfTasksAndRemaining(1)
|
||||
syncStrategy?.startSync { result in
|
||||
os_log(.debug, log: log, "Sync took %.3f seconds", -date.timeIntervalSinceNow)
|
||||
|
||||
let operation = FeedlySyncAllOperation(account: account, credentials: credentials, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetch, log: log)
|
||||
|
||||
let date = Date()
|
||||
operation.syncCompletionHandler = { [weak self] result in
|
||||
if case .success = result {
|
||||
self?.accountMetadata?.lastArticleFetch = date
|
||||
}
|
||||
|
||||
os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow)
|
||||
progress.completeTask()
|
||||
completion(result)
|
||||
}
|
||||
|
||||
currentSyncAllOperation = operation
|
||||
|
||||
operationQueue.addOperation(operation)
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
|
||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
// Ensure remote articles have the same status as they do locally.
|
||||
let send = FeedlySendArticleStatusesOperation(database: database, caller: caller, log: log)
|
||||
let send = FeedlySendArticleStatusesOperation(database: database, service: caller, log: log)
|
||||
send.completionBlock = {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
OperationQueue.main.addOperation(send)
|
||||
operationQueue.addOperation(send)
|
||||
}
|
||||
|
||||
/// Attempts to ensure local articles have the same status as they do remotely.
|
||||
|
@ -111,10 +159,33 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
///
|
||||
/// - Parameter account: The account whose articles have a remote status.
|
||||
/// - Parameter completion: Call on the main queue.
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
|
||||
refreshAll(for: account) { _ in
|
||||
completion()
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard let credentials = credentials else {
|
||||
return completion(.success(()))
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
|
||||
|
||||
group.enter()
|
||||
syncUnread.completionBlock = {
|
||||
group.leave()
|
||||
|
||||
}
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: caller, log: log)
|
||||
|
||||
group.enter()
|
||||
syncStarred.completionBlock = {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
operationQueue.addOperations([syncUnread, syncStarred], waitUntilFinished: false)
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
@ -223,7 +294,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
var createFeedRequest: FeedlyAddFeedRequest?
|
||||
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
let progress = refreshProgress
|
||||
progress.addToNumberOfTasksAndRemaining(1)
|
||||
|
@ -239,14 +310,14 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID }
|
||||
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name)))
|
||||
return
|
||||
}
|
||||
|
||||
let feedId = FeedlyFeedResourceId(id: feed.feedID)
|
||||
let feedId = FeedlyFeedResourceId(id: feed.webFeedID)
|
||||
let editedNameBefore = feed.editedName
|
||||
|
||||
// Adding an existing feed updates it.
|
||||
|
@ -268,7 +339,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
var addFeedRequest: FeedlyAddFeedRequest?
|
||||
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
let progress = refreshProgress
|
||||
progress.addToNumberOfTasksAndRemaining(1)
|
||||
|
@ -291,62 +362,62 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
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
|
||||
caller.removeFeed(feed.webFeedID, fromCollectionWith: collectionId) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
folder.addFeed(feed)
|
||||
folder.addWebFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
folder.removeFeed(feed)
|
||||
folder.removeWebFeed(feed)
|
||||
}
|
||||
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
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
|
||||
addWebFeed(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
|
||||
self?.removeWebFeed(for: account, with: feed, from: from) { removeResult in
|
||||
switch removeResult {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure:
|
||||
from.addFeed(feed)
|
||||
from.addWebFeed(feed)
|
||||
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
from.addFeed(feed)
|
||||
to.removeFeed(feed)
|
||||
from.addWebFeed(feed)
|
||||
to.removeWebFeed(feed)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// optimistically move the feed, undoing as appropriate to the failure
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
from.removeWebFeed(feed)
|
||||
to.addWebFeed(feed)
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||
account.addWebFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
@ -355,7 +426,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
@ -369,12 +440,12 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
for feed in folder.topLevelFeeds {
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
|
||||
folder.topLevelFeeds.remove(feed)
|
||||
folder.topLevelWebFeeds.remove(feed)
|
||||
|
||||
group.enter()
|
||||
restoreFeed(for: account, feed: feed, container: folder) { result in
|
||||
restoreWebFeed(for: account, feed: feed, container: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
|
@ -402,7 +473,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
os_log(.debug, log: log, "Marking %@ as %@.", articles.map { $0.title }, syncStatuses)
|
||||
|
||||
if database.selectPendingCount() > 100 {
|
||||
sendArticleStatus(for: account) { }
|
||||
sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
|
||||
return account.update(articles, statusKey: statusKey, flag: flag)
|
||||
|
@ -411,13 +482,18 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
func accountDidInitialize(_ account: Account) {
|
||||
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
|
||||
|
||||
syncStrategy = FeedlySyncStrategy(account: account,
|
||||
caller: caller,
|
||||
database: database,
|
||||
log: log)
|
||||
let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log)
|
||||
operationQueue.addOperation(refreshAccessToken)
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
let logout = FeedlyLogoutOperation(account: account, service: caller, log: log)
|
||||
// Dispatch on the main queue because the lifetime of the account delegate is uncertain.
|
||||
OperationQueue.main.addOperation(logout)
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
fatalError()
|
||||
assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.")
|
||||
completion(.success(credentials))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
|||
case unableToAddFolder(String)
|
||||
case unableToRenameFolder(String, String)
|
||||
case unableToRemoveFolder(String)
|
||||
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
|
||||
case unableToMoveFeedBetweenFolders(WebFeed, Folder, Folder)
|
||||
case addFeedChooseFolder
|
||||
case addFeedInvalidFolder(Folder)
|
||||
case unableToRenameFeed(String, String)
|
||||
case unableToRemoveFeed(Feed)
|
||||
case unableToRemoveFeed(WebFeed)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
|
|
|
@ -29,7 +29,7 @@ final class FeedlyAddFeedRequest {
|
|||
self.resourceProvider = resourceProvider
|
||||
}
|
||||
|
||||
var completionHandler: ((Result<Feed, Error>) -> ())?
|
||||
var completionHandler: ((Result<WebFeed, Error>) -> ())?
|
||||
var error: Error?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
|
@ -37,17 +37,17 @@ final class FeedlyAddFeedRequest {
|
|||
}
|
||||
}
|
||||
|
||||
func addNewFeed(at url: String, name: String? = nil, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func addNewFeed(at url: String, name: String? = nil, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
let resource = FeedlyFeedResourceId(url: url)
|
||||
self.start(resource: resource, name: name, refreshes: true, completion: completion)
|
||||
}
|
||||
|
||||
func add(existing feed: Feed, name: String? = nil, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
let resource = FeedlyFeedResourceId(id: feed.feedID)
|
||||
func add(existing feed: WebFeed, name: String? = nil, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
let resource = FeedlyFeedResourceId(id: feed.webFeedID)
|
||||
self.start(resource: resource, name: name, refreshes: false, completion: completion)
|
||||
}
|
||||
|
||||
private func start(resource: FeedlyFeedResourceId, name: String?, refreshes: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
private func start(resource: FeedlyFeedResourceId, name: String?, refreshes: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
|
||||
let (folder, collectionId): (Folder, String)
|
||||
do {
|
||||
|
@ -68,9 +68,9 @@ final class FeedlyAddFeedRequest {
|
|||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log)
|
||||
createFeeds.addDependency(addRequest)
|
||||
|
||||
let getStream: FeedlyGetStreamOperation? = {
|
||||
let getStream: FeedlyGetStreamContentsOperation? = {
|
||||
if refreshes {
|
||||
let op = FeedlyGetStreamOperation(account: account, resourceProvider: addRequest, caller: caller, newerThan: nil)
|
||||
let op = FeedlyGetStreamContentsOperation(account: account, resourceProvider: addRequest, service: caller, newerThan: nil)
|
||||
op.addDependency(createFeeds)
|
||||
return op
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ final class FeedlyAddFeedRequest {
|
|||
|
||||
let organiseByFeed: FeedlyOrganiseParsedItemsByFeedOperation? = {
|
||||
if let getStream = getStream {
|
||||
let op = FeedlyOrganiseParsedItemsByFeedOperation(account: account, entryProvider: getStream, log: log)
|
||||
let op = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: getStream, log: log)
|
||||
op.addDependency(getStream)
|
||||
return op
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ final class FeedlyAddFeedRequest {
|
|||
if let error = delegate.error {
|
||||
handler(.failure(error))
|
||||
|
||||
} else if let feed = folder.existingFeed(withFeedID: resource.id) {
|
||||
} else if let feed = folder.existingWebFeed(withWebFeedID: resource.id) {
|
||||
handler(.success(feed))
|
||||
|
||||
} else {
|
||||
|
|
|
@ -25,7 +25,7 @@ struct FeedlyFeedContainerValidator {
|
|||
throw FeedlyAccountDelegateError.notLoggedIn
|
||||
}
|
||||
|
||||
let uncategorized = FeedlyCategoryResourceId.uncategorized(for: userId)
|
||||
let uncategorized = FeedlyCategoryResourceId.Global.uncategorized(for: userId)
|
||||
|
||||
guard collectionId != uncategorized.id else {
|
||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||
|
|
|
@ -84,7 +84,7 @@ struct FeedlyEntryParser {
|
|||
|
||||
var parsedItemRepresentation: ParsedItem {
|
||||
return ParsedItem(syncServiceID: id,
|
||||
uniqueID: id,
|
||||
uniqueID: id, // This value seems to get ignored or replaced.
|
||||
feedURL: feedUrl,
|
||||
url: nil,
|
||||
externalURL: externalUrl,
|
||||
|
|
|
@ -43,19 +43,32 @@ extension FeedlyFeedResourceId {
|
|||
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)
|
||||
enum Global {
|
||||
|
||||
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 {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/category/global.all"
|
||||
return FeedlyCategoryResourceId(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedlyTagResourceId: FeedlyResourceId {
|
||||
var id: String
|
||||
|
||||
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)
|
||||
enum Global {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,13 @@ import Foundation
|
|||
|
||||
struct FeedlyStream: Decodable {
|
||||
var id: String
|
||||
var timestamp: Date?
|
||||
|
||||
/// Of the most recent entry for this stream (regardless of continuation, newerThan, etc).
|
||||
var updated: Date?
|
||||
|
||||
/// Optional string 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.
|
||||
var continuation: String?
|
||||
var items: [FeedlyEntry]
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue