Put feeds in folders (code taken from Feedbin)
This commit is contained in:
parent
8e99f8deea
commit
6b38c07654
@ -11,17 +11,18 @@ import RSCore
|
|||||||
import RSParser
|
import RSParser
|
||||||
|
|
||||||
typealias NewsBlurFeed = NewsBlurFeedsResponse.Feed
|
typealias NewsBlurFeed = NewsBlurFeedsResponse.Feed
|
||||||
|
typealias NewsBlurFolder = NewsBlurFeedsResponse.Folder
|
||||||
|
|
||||||
struct NewsBlurFeedsResponse: Decodable {
|
struct NewsBlurFeedsResponse: Decodable {
|
||||||
let feeds: [Feed]
|
let feeds: [Feed]
|
||||||
let folders: [Folder]
|
let folders: [Folder]
|
||||||
|
|
||||||
struct Feed: Hashable, Codable {
|
struct Feed: Hashable, Codable {
|
||||||
let title: String
|
let name: String
|
||||||
let feedID: Int
|
let feedID: Int
|
||||||
let feedURL: String
|
let feedURL: String
|
||||||
let siteURL: String?
|
let homepageURL: String?
|
||||||
let favicon: String?
|
let faviconURL: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Folder: Hashable, Codable {
|
struct Folder: Hashable, Codable {
|
||||||
@ -30,6 +31,11 @@ struct NewsBlurFeedsResponse: Decodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NewsBlurFolderRelationship: Codable {
|
||||||
|
let folderName: String
|
||||||
|
let feedID: Int
|
||||||
|
}
|
||||||
|
|
||||||
extension NewsBlurFeedsResponse {
|
extension NewsBlurFeedsResponse {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case feeds = "feeds"
|
case feeds = "feeds"
|
||||||
@ -65,10 +71,16 @@ extension NewsBlurFeedsResponse {
|
|||||||
|
|
||||||
extension NewsBlurFeedsResponse.Feed {
|
extension NewsBlurFeedsResponse.Feed {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case title = "feed_title"
|
case name = "feed_title"
|
||||||
case feedID = "id"
|
case feedID = "id"
|
||||||
case feedURL = "feed_address"
|
case feedURL = "feed_address"
|
||||||
case siteURL = "feed_link"
|
case homepageURL = "feed_link"
|
||||||
case favicon = "favicon_url"
|
case faviconURL = "favicon_url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsBlurFeedsResponse.Folder {
|
||||||
|
var asRelationships: [NewsBlurFolderRelationship] {
|
||||||
|
return feedIDs.map { NewsBlurFolderRelationship(folderName: name, feedID: $0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ final class NewsBlurAPICaller: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveFeeds(completion: @escaping (Result<[NewsBlurFeed]?, Error>) -> Void) {
|
func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
|
||||||
let url = baseURL
|
let url = baseURL
|
||||||
.appendingPathComponent("reader/feeds")
|
.appendingPathComponent("reader/feeds")
|
||||||
.appendingQueryItems([
|
.appendingQueryItems([
|
||||||
@ -101,7 +101,7 @@ final class NewsBlurAPICaller: NSObject {
|
|||||||
transport.send(request: request, resultType: NewsBlurFeedsResponse.self) { result in
|
transport.send(request: request, resultType: NewsBlurFeedsResponse.self) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success((_, let payload)):
|
case .success((_, let payload)):
|
||||||
completion(.success(payload?.feeds))
|
completion(.success((payload?.feeds, payload?.folders)))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
|
@ -231,10 +231,15 @@ extension NewsBlurAccountDelegate {
|
|||||||
|
|
||||||
caller.retrieveFeeds { result in
|
caller.retrieveFeeds { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let feeds):
|
case .success((let feeds, let folders)):
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
self.syncFeeds(account, feeds)
|
BatchUpdate.shared.perform {
|
||||||
|
self.syncFolders(account, folders)
|
||||||
|
self.syncFeeds(account, feeds)
|
||||||
|
self.syncFeedFolderRelationship(account, folders)
|
||||||
|
}
|
||||||
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
@ -242,8 +247,46 @@ extension NewsBlurAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func syncFolders(_ account: Account, _ folders: [NewsBlurFolder]?) {
|
||||||
|
guard let folders = folders else { return }
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
os_log(.debug, log: log, "Syncing folders with %ld folders.", folders.count)
|
||||||
|
|
||||||
|
let folderNames = folders.map { $0.name }
|
||||||
|
|
||||||
|
// Delete any folders not at NewsBlur
|
||||||
|
if let folders = account.folders {
|
||||||
|
folders.forEach { folder in
|
||||||
|
if !folderNames.contains(folder.name ?? "") {
|
||||||
|
for feed in folder.topLevelWebFeeds {
|
||||||
|
account.addWebFeed(feed)
|
||||||
|
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||||
|
}
|
||||||
|
account.removeFolder(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountFolderNames: [String] = {
|
||||||
|
if let folders = account.folders {
|
||||||
|
return folders.map { $0.name ?? "" }
|
||||||
|
} else {
|
||||||
|
return [String]()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Make any folders NewsBlur has, but we don't
|
||||||
|
folderNames.forEach { folderName in
|
||||||
|
if !accountFolderNames.contains(folderName) {
|
||||||
|
_ = account.ensureFolder(with: folderName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func syncFeeds(_ account: Account, _ feeds: [NewsBlurFeed]?) {
|
private func syncFeeds(_ account: Account, _ feeds: [NewsBlurFeed]?) {
|
||||||
guard let feeds = feeds else { return }
|
guard let feeds = feeds else { return }
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
os_log(.debug, log: log, "Syncing feeds with %ld feeds.", feeds.count)
|
os_log(.debug, log: log, "Syncing feeds with %ld feeds.", feeds.count)
|
||||||
|
|
||||||
@ -272,13 +315,12 @@ extension NewsBlurAccountDelegate {
|
|||||||
let subFeedId = String(feed.feedID)
|
let subFeedId = String(feed.feedID)
|
||||||
|
|
||||||
if let webFeed = account.existingWebFeed(withWebFeedID: subFeedId) {
|
if let webFeed = account.existingWebFeed(withWebFeedID: subFeedId) {
|
||||||
webFeed.name = feed.title
|
webFeed.name = feed.name
|
||||||
// If the name has been changed on the server remove the locally edited name
|
// If the name has been changed on the server remove the locally edited name
|
||||||
webFeed.editedName = nil
|
webFeed.editedName = nil
|
||||||
webFeed.homePageURL = feed.siteURL
|
webFeed.homePageURL = feed.homepageURL
|
||||||
webFeed.subscriptionID = String(feed.feedID)
|
webFeed.subscriptionID = String(feed.feedID)
|
||||||
webFeed.faviconURL = feed.favicon
|
webFeed.faviconURL = feed.faviconURL
|
||||||
webFeed.iconURL = feed.favicon
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
feedsToAdd.insert(feed)
|
feedsToAdd.insert(feed)
|
||||||
@ -287,12 +329,104 @@ extension NewsBlurAccountDelegate {
|
|||||||
|
|
||||||
// Actually add feeds all in one go, so we don’t trigger various rebuilding things that Account does.
|
// Actually add feeds all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||||
feedsToAdd.forEach { feed in
|
feedsToAdd.forEach { feed in
|
||||||
let webFeed = account.createWebFeed(with: feed.title, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.siteURL)
|
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homepageURL)
|
||||||
webFeed.subscriptionID = String(feed.feedID)
|
webFeed.subscriptionID = String(feed.feedID)
|
||||||
account.addWebFeed(webFeed)
|
account.addWebFeed(webFeed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func syncFeedFolderRelationship(_ account: Account, _ folders: [NewsBlurFolder]?) {
|
||||||
|
guard let folders = folders else { return }
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
os_log(.debug, log: log, "Syncing folders with %ld folders.", folders.count)
|
||||||
|
|
||||||
|
// Set up some structures to make syncing easier
|
||||||
|
let relationships = folders.map({ $0.asRelationships }).flatMap { $0 }
|
||||||
|
let folderDict = nameToFolderDictionary(with: account.folders)
|
||||||
|
let foldersDict = relationships.reduce([String: [NewsBlurFolderRelationship]]()) { (dict, relationship) in
|
||||||
|
var feedInFolders = dict
|
||||||
|
if var feedInFolder = feedInFolders[relationship.folderName] {
|
||||||
|
feedInFolder.append(relationship)
|
||||||
|
feedInFolders[relationship.folderName] = feedInFolder
|
||||||
|
} else {
|
||||||
|
feedInFolders[relationship.folderName] = [relationship]
|
||||||
|
}
|
||||||
|
return feedInFolders
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync the folders
|
||||||
|
for (folderName, folderRelationships) in foldersDict {
|
||||||
|
guard let folder = folderDict[folderName] else { return }
|
||||||
|
|
||||||
|
let folderFeedIDs = folderRelationships.map { String($0.feedID) }
|
||||||
|
|
||||||
|
// Move any feeds not in the folder to the account
|
||||||
|
for feed in folder.topLevelWebFeeds {
|
||||||
|
if !folderFeedIDs.contains(feed.webFeedID) {
|
||||||
|
folder.removeWebFeed(feed)
|
||||||
|
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||||
|
account.addWebFeed(feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any feeds not in the folder
|
||||||
|
let folderFeedIds = folder.topLevelWebFeeds.map { $0.webFeedID }
|
||||||
|
|
||||||
|
for relationship in folderRelationships {
|
||||||
|
let folderFeedID = String(relationship.feedID)
|
||||||
|
if !folderFeedIds.contains(folderFeedID) {
|
||||||
|
guard let feed = account.existingWebFeed(withWebFeedID: folderFeedID) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
saveFolderRelationship(for: feed, withFolderName: folderName, id: relationship.folderName)
|
||||||
|
folder.addWebFeed(feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let folderFeedIDs = Set(relationships.map { String($0.feedID) })
|
||||||
|
|
||||||
|
// Remove all feeds from the account container that have a tag
|
||||||
|
for feed in account.topLevelWebFeeds {
|
||||||
|
if folderFeedIDs.contains(feed.webFeedID) {
|
||||||
|
account.removeWebFeed(feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) {
|
||||||
|
if var folderRelationship = feed.folderRelationship {
|
||||||
|
folderRelationship[folderName] = nil
|
||||||
|
feed.folderRelationship = folderRelationship
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) {
|
||||||
|
if var folderRelationship = feed.folderRelationship {
|
||||||
|
folderRelationship[folderName] = id
|
||||||
|
feed.folderRelationship = folderRelationship
|
||||||
|
} else {
|
||||||
|
feed.folderRelationship = [folderName: id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nameToFolderDictionary(with folders: Set<Folder>?) -> [String: Folder] {
|
||||||
|
guard let folders = folders else {
|
||||||
|
return [String: Folder]()
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = [String: Folder]()
|
||||||
|
for folder in folders {
|
||||||
|
let name = folder.name ?? ""
|
||||||
|
if d[name] == nil {
|
||||||
|
d[name] = folder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
private func refreshUnreadStories(for account: Account, hashes: [NewsBlurStoryHash]?, updateFetchDate: Date?, completion: @escaping (Result<Void, Error>) -> Void) {
|
private func refreshUnreadStories(for account: Account, hashes: [NewsBlurStoryHash]?, updateFetchDate: Date?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let hashes = hashes, !hashes.isEmpty else {
|
guard let hashes = hashes, !hashes.isEmpty else {
|
||||||
if let lastArticleFetch = updateFetchDate {
|
if let lastArticleFetch = updateFetchDate {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user