This commit is contained in:
Brent Simmons 2020-10-31 23:10:22 -07:00
commit e9f7cdb0c6
5 changed files with 108 additions and 24 deletions

View File

@ -28,6 +28,11 @@ public enum AccountBehavior: Equatable {
*/ */
case disallowFeedInRootFolder case disallowFeedInRootFolder
/**
Account doesn't support a feed being in more than one folder.
*/
case disallowFeedInMultipleFolders
/** /**
Account doesn't support folders Account doesn't support folders
*/ */

View File

@ -29,7 +29,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI") private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI")
var behaviors: AccountBehaviors { var behaviors: AccountBehaviors {
var behaviors: AccountBehaviors = [.disallowOPMLImports] var behaviors: AccountBehaviors = [.disallowOPMLImports, .disallowFeedInMultipleFolders]
if variant == .freshRSS { if variant == .freshRSS {
behaviors.append(.disallowFeedInRootFolder) behaviors.append(.disallowFeedInRootFolder)
} }
@ -186,8 +186,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
caller.retrieveItemIDs(type: .unread) { result in caller.retrieveItemIDs(type: .unread) { result in
switch result { switch result {
case .success(let articleIDs): case .success(let articleIDs):
self.syncArticleReadState(account: account, articleIDs: articleIDs) self.syncArticleReadState(account: account, articleIDs: articleIDs) {
group.leave() group.leave()
}
case .failure(let error): case .failure(let error):
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription) os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
group.leave() group.leave()
@ -199,8 +200,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
caller.retrieveItemIDs(type: .starred) { result in caller.retrieveItemIDs(type: .starred) { result in
switch result { switch result {
case .success(let articleIDs): case .success(let articleIDs):
self.syncArticleStarredState(account: account, articleIDs: articleIDs) self.syncArticleStarredState(account: account, articleIDs: articleIDs) {
group.leave() group.leave()
}
case .failure(let error): case .failure(let error):
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
group.leave() group.leave()
@ -918,7 +920,7 @@ private extension ReaderAPIAccountDelegate {
let group = DispatchGroup() let group = DispatchGroup()
let articleIDs = Array(fetchedArticleIDs) let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 100) let chunkedArticleIDs = articleIDs.chunked(into: 150)
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1) self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
@ -1001,7 +1003,7 @@ private extension ReaderAPIAccountDelegate {
} }
func syncArticleReadState(account: Account, articleIDs: [String]?) { func syncArticleReadState(account: Account, articleIDs: [String]?, completion: @escaping (() -> Void)) {
guard let articleIDs = articleIDs else { guard let articleIDs = articleIDs else {
return return
} }
@ -1016,13 +1018,25 @@ private extension ReaderAPIAccountDelegate {
return return
} }
let group = DispatchGroup()
// Mark articles as unread // Mark articles as unread
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs) let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
account.markAsUnread(deltaUnreadArticleIDs) group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
// Mark articles as read // Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs) let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
account.markAsRead(deltaReadArticleIDs) group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
} }
} }
@ -1037,7 +1051,7 @@ private extension ReaderAPIAccountDelegate {
} }
func syncArticleStarredState(account: Account, articleIDs: [String]?) { func syncArticleStarredState(account: Account, articleIDs: [String]?, completion: @escaping (() -> Void)) {
guard let articleIDs = articleIDs else { guard let articleIDs = articleIDs else {
return return
} }
@ -1052,13 +1066,25 @@ private extension ReaderAPIAccountDelegate {
return return
} }
let group = DispatchGroup()
// Mark articles as starred // Mark articles as starred
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs) let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
account.markAsStarred(deltaStarredArticleIDs) group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
// Mark articles as unstarred // Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs) let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
account.markAsUnstarred(deltaUnstarredArticleIDs) group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
} }
} }

View File

@ -631,52 +631,76 @@ private extension SidebarOutlineDataSource {
return false return false
} }
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Bool { func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeed: PasteboardWebFeed) -> Bool {
return violatesAccountSpecificBehavior(parentNode, Set([draggedFeed])) return violatesAccountSpecificBehavior(dropTargetNode, Set([draggedFeed]))
} }
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool { func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
if violatesDisallowFeedInRootFolder(parentNode) { if violatesDisallowFeedInRootFolder(dropTargetNode) {
return true return true
} }
if violatesDisallowFeedCopyInRootFolder(parentNode, draggedFeeds) { if violatesDisallowFeedCopyInRootFolder(dropTargetNode, draggedFeeds) {
return true
}
if violatesDisallowFeedInMultipleFolders(dropTargetNode, draggedFeeds) {
return true return true
} }
return false return false
} }
func violatesDisallowFeedInRootFolder(_ parentNode: Node) -> Bool { func violatesDisallowFeedInRootFolder(_ dropTargetNode: Node) -> Bool {
guard let parentAccount = nodeAccount(parentNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else { guard let parentAccount = nodeAccount(dropTargetNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else {
return false return false
} }
if parentNode.representedObject is Account { if dropTargetNode.representedObject is Account {
return true return true
} }
return false return false
} }
func violatesDisallowFeedCopyInRootFolder(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool { func violatesDisallowFeedCopyInRootFolder(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
guard let parentAccount = nodeAccount(parentNode), parentAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else { guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else {
return false return false
} }
for draggedFeed in draggedFeeds { for draggedFeed in draggedFeeds {
if parentAccount.accountID != draggedFeed.accountID { if dropTargetAccount.accountID != draggedFeed.accountID {
return false return false
} }
} }
if parentNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) { if dropTargetNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
return true return true
} }
return false return false
} }
func violatesDisallowFeedInMultipleFolders(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedInMultipleFolders) else {
return false
}
for draggedFeed in draggedFeeds {
if dropTargetAccount.accountID == draggedFeed.accountID {
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
return true
}
} else {
if dropTargetAccount.hasWebFeed(withURL: draggedFeed.url) {
return true
}
}
}
return false
}
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Int { func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Int {
let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed) let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed)
let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil) let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil)

View File

@ -18,6 +18,7 @@ final class MasterFeedTableViewIdentifier: NSObject, NSCopying {
let isEditable: Bool let isEditable: Bool
let isPsuedoFeed: Bool let isPsuedoFeed: Bool
let isAccount: Bool
let isFolder: Bool let isFolder: Bool
let isWebFeed: Bool let isWebFeed: Bool
@ -26,6 +27,19 @@ final class MasterFeedTableViewIdentifier: NSObject, NSCopying {
let unreadCount: Int let unreadCount: Int
let childCount: Int let childCount: Int
var account: Account? {
if isAccount, let containerID = containerID {
return AccountManager.shared.existingContainer(with: containerID) as? Account
}
if isFolder, let parentContainerID = parentContainerID {
return AccountManager.shared.existingContainer(with: parentContainerID) as? Account
}
if isWebFeed, let feedID = feedID {
return (AccountManager.shared.existingFeed(with: feedID) as? WebFeed)?.account
}
return nil
}
init(node: Node, unreadCount: Int) { init(node: Node, unreadCount: Int) {
let feed = node.representedObject as! Feed let feed = node.representedObject as! Feed
self.feedID = feed.feedID self.feedID = feed.feedID
@ -34,6 +48,7 @@ final class MasterFeedTableViewIdentifier: NSObject, NSCopying {
self.isEditable = !(node.representedObject is PseudoFeed) self.isEditable = !(node.representedObject is PseudoFeed)
self.isPsuedoFeed = node.representedObject is PseudoFeed self.isPsuedoFeed = node.representedObject is PseudoFeed
self.isAccount = node.representedObject is Account
self.isFolder = node.representedObject is Folder self.isFolder = node.representedObject is Folder
self.isWebFeed = node.representedObject is WebFeed self.isWebFeed = node.representedObject is WebFeed
self.nameForDisplay = feed.nameForDisplay self.nameForDisplay = feed.nameForDisplay

View File

@ -22,10 +22,24 @@ extension MasterFeedViewController: UITableViewDropDelegate {
destIndexPath.section > 0, destIndexPath.section > 0,
tableView.hasActiveDrag, tableView.hasActiveDrag,
let destIdentifier = dataSource.itemIdentifier(for: destIndexPath), let destIdentifier = dataSource.itemIdentifier(for: destIndexPath),
let destAccount = destIdentifier.account,
let destCell = tableView.cellForRow(at: destIndexPath) else { let destCell = tableView.cellForRow(at: destIndexPath) else {
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
} }
// Validate account specific behaviors...
if destAccount.behaviors.contains(.disallowFeedInRootFolder) && destIdentifier.isAccount {
return UITableViewDropProposal(operation: .forbidden)
}
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
let sourceFeedID = (session.localDragSession?.items.first?.localObject as? MasterFeedTableViewIdentifier)?.feedID,
let sourceWebFeed = AccountManager.shared.existingFeed(with: sourceFeedID) as? WebFeed,
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
return UITableViewDropProposal(operation: .forbidden)
}
// Determine the correct drop proposal
if destIdentifier.isFolder { if destIdentifier.isFolder {
if session.location(in: destCell).y >= 0 { if session.location(in: destCell).y >= 0 {
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)