Refactor Diffable Datasources out of the Sidebar
This commit is contained in:
parent
fd3a3cf3b3
commit
bbc7230e76
@ -17,6 +17,7 @@ public enum ReadFilterType {
|
||||
|
||||
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
|
||||
|
||||
var account: Account? { get }
|
||||
var defaultReadFilterType: ReadFilterType { get }
|
||||
|
||||
}
|
||||
|
@ -183,7 +183,6 @@
|
||||
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513C5CE8232571C2003D4054 /* ShareViewController.swift */; };
|
||||
513C5CEC232571C2003D4054 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 513C5CEA232571C2003D4054 /* MainInterface.storyboard */; };
|
||||
513C5CF0232571C2003D4054 /* NetNewsWire iOS Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
513CCF2524880C1500C55709 /* MasterFeedTableViewIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513CCF08248808BA00C55709 /* MasterFeedTableViewIdentifier.swift */; };
|
||||
513F325C2593ECF40003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F325B2593ECF40003048F /* RSCore */; };
|
||||
513F325D2593ECF40003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F325B2593ECF40003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
513F32712593EE6F0003048F /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32702593EE6F0003048F /* Articles */; };
|
||||
@ -242,7 +241,6 @@
|
||||
516244E3241E19F000B61C47 /* ColorPaletteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */; };
|
||||
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; };
|
||||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; };
|
||||
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; };
|
||||
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */; };
|
||||
516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */; };
|
||||
516A09392360A2AE00EAE89B /* SettingsComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */; };
|
||||
@ -281,6 +279,8 @@
|
||||
5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
|
||||
5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
|
||||
5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
|
||||
5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1D92720205F00888867 /* ShadowTableChanges.swift */; };
|
||||
5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */; };
|
||||
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
|
||||
519CA8E525841DB700EB079A /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 519CA8E425841DB700EB079A /* CrashReporter */; };
|
||||
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; };
|
||||
@ -409,8 +409,6 @@
|
||||
51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; };
|
||||
51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; };
|
||||
51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; };
|
||||
51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; };
|
||||
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */; };
|
||||
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; };
|
||||
51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; };
|
||||
51DEE81326FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; };
|
||||
@ -1214,7 +1212,6 @@
|
||||
513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
513CCF08248808BA00C55709 /* MasterFeedTableViewIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewIdentifier.swift; sourceTree = "<group>"; };
|
||||
5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorViewController.swift; sourceTree = "<group>"; };
|
||||
5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailIconSchemeHandler.swift; sourceTree = "<group>"; };
|
||||
5142192923522B5500E07E2C /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||
@ -1245,7 +1242,6 @@
|
||||
516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteTableViewController.swift; sourceTree = "<group>"; };
|
||||
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = "<group>"; };
|
||||
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = "<group>"; };
|
||||
51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
|
||||
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingPreviewParameters.swift; sourceTree = "<group>"; };
|
||||
516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsComboTableViewCell.xib; sourceTree = "<group>"; };
|
||||
516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsComboTableViewCell.swift; sourceTree = "<group>"; };
|
||||
@ -1279,6 +1275,8 @@
|
||||
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
|
||||
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
|
||||
5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RedditFeedProvider-Extensions.swift"; sourceTree = "<group>"; };
|
||||
5195C1D92720205F00888867 /* ShadowTableChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowTableChanges.swift; sourceTree = "<group>"; };
|
||||
5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedRowIdentifier.swift; sourceTree = "<group>"; };
|
||||
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
|
||||
519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = "<group>"; };
|
||||
@ -1343,8 +1341,6 @@
|
||||
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
||||
51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
|
||||
51DC07972552083500A3F79F /* ArticleTextSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTextSize.swift; sourceTree = "<group>"; };
|
||||
51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = "<group>"; };
|
||||
51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = "<group>"; };
|
||||
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = "<group>"; };
|
||||
51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Appanoose.nnwtheme; sourceTree = "<group>"; };
|
||||
51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Promenade.nnwtheme; sourceTree = "<group>"; };
|
||||
@ -2073,11 +2069,8 @@
|
||||
51C45264226508F600C03939 /* MasterFeedViewController.swift */,
|
||||
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */,
|
||||
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */,
|
||||
51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */,
|
||||
51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */,
|
||||
51CE1C0A23622006005548FC /* RefreshProgressView.swift */,
|
||||
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */,
|
||||
51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */,
|
||||
51C45260226508F600C03939 /* Cell */,
|
||||
);
|
||||
path = MasterFeed;
|
||||
@ -2086,9 +2079,9 @@
|
||||
51C45260226508F600C03939 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */,
|
||||
51C45262226508F600C03939 /* MasterFeedTableViewCell.swift */,
|
||||
51C45263226508F600C03939 /* MasterFeedTableViewCellLayout.swift */,
|
||||
513CCF08248808BA00C55709 /* MasterFeedTableViewIdentifier.swift */,
|
||||
512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */,
|
||||
516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */,
|
||||
51C45261226508F600C03939 /* MasterFeedUnreadCountView.swift */,
|
||||
@ -2698,6 +2691,7 @@
|
||||
840D617E2029031C009BC708 /* AppDelegate.swift */,
|
||||
519E743422C663F900A78E47 /* SceneDelegate.swift */,
|
||||
5126EE96226CB48A00C22AFC /* SceneCoordinator.swift */,
|
||||
5195C1D92720205F00888867 /* ShadowTableChanges.swift */,
|
||||
514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */,
|
||||
511D4410231FC02D00FB1562 /* KeyboardManager.swift */,
|
||||
51C45254226507D200C03939 /* AppAssets.swift */,
|
||||
@ -4054,6 +4048,7 @@
|
||||
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */,
|
||||
51EF0F79227716380050506E /* ColorHash.swift in Sources */,
|
||||
51F9F3FB23DFB25700A314FD /* Animations.swift in Sources */,
|
||||
5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */,
|
||||
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */,
|
||||
B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */,
|
||||
518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */,
|
||||
@ -4146,7 +4141,6 @@
|
||||
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
|
||||
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
||||
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
|
||||
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
|
||||
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
@ -4178,6 +4172,7 @@
|
||||
512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */,
|
||||
512392C224E33A3C00F11704 /* RedditEnterDetailTableViewController.swift in Sources */,
|
||||
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
|
||||
5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */,
|
||||
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
|
||||
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
|
||||
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
|
||||
@ -4196,7 +4191,6 @@
|
||||
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
|
||||
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
||||
17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */,
|
||||
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
|
||||
17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */,
|
||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
||||
@ -4214,7 +4208,6 @@
|
||||
FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */,
|
||||
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
|
||||
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
|
||||
51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */,
|
||||
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
|
||||
5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */,
|
||||
515A517C243E90260089E588 /* ExtensionPointManager.swift in Sources */,
|
||||
@ -4226,7 +4219,6 @@
|
||||
511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */,
|
||||
51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */,
|
||||
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
|
||||
513CCF2524880C1500C55709 /* MasterFeedTableViewIdentifier.swift in Sources */,
|
||||
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */,
|
||||
51934CCB230F599B006127BE /* InteractiveNavigationController.swift in Sources */,
|
||||
769F2ED513DA03EE75B993A8 /* NewsBlurAccountViewController.swift in Sources */,
|
||||
|
@ -14,6 +14,8 @@ import Account
|
||||
|
||||
final class SmartFeed: PseudoFeed {
|
||||
|
||||
var account: Account? = nil
|
||||
|
||||
public var defaultReadFilterType: ReadFilterType {
|
||||
return .none
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ import ArticlesDatabase
|
||||
// This just shows the global unread count, which appDelegate already has. Easy.
|
||||
|
||||
final class UnreadFeed: PseudoFeed {
|
||||
|
||||
var account: Account? = nil
|
||||
|
||||
public var defaultReadFilterType: ReadFilterType {
|
||||
return .alwaysRead
|
||||
|
23
iOS/MasterFeed/Cell/MasterFeedRowIdentifier.swift
Normal file
23
iOS/MasterFeed/Cell/MasterFeedRowIdentifier.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// MasterFeedRowIdentifier.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 10/20/21.
|
||||
// Copyright © 2021 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MasterFeedRowIdentifier: NSObject, NSCopying {
|
||||
|
||||
var indexPath: IndexPath
|
||||
|
||||
init(indexPath: IndexPath) {
|
||||
self.indexPath = indexPath
|
||||
}
|
||||
|
||||
func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
//
|
||||
// MasterFeedDataSource.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 8/28/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
class MasterFeedDataSource: UITableViewDiffableDataSource<Int, MasterFeedTableViewIdentifier> {
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let identifier = itemIdentifier(for: indexPath), identifier.isEditable else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
//
|
||||
// MasterFeedDataSourceOperation.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 2/23/20.
|
||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
import RSTree
|
||||
|
||||
class MasterFeedDataSourceOperation: MainThreadOperation {
|
||||
|
||||
// MainThreadOperation
|
||||
public var isCanceled = false
|
||||
public var id: Int?
|
||||
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||
public var name: String? = "MasterFeedDataSourceOperation"
|
||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, MasterFeedTableViewIdentifier>
|
||||
private var snapshot: NSDiffableDataSourceSnapshot<Int, MasterFeedTableViewIdentifier>
|
||||
private var animating: Bool
|
||||
|
||||
init(dataSource: UITableViewDiffableDataSource<Int, MasterFeedTableViewIdentifier>, snapshot: NSDiffableDataSourceSnapshot<Int, MasterFeedTableViewIdentifier>, animating: Bool) {
|
||||
self.dataSource = dataSource
|
||||
self.snapshot = snapshot
|
||||
self.animating = animating
|
||||
}
|
||||
|
||||
func run() {
|
||||
dataSource.apply(snapshot, animatingDifferences: animating) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -13,11 +13,11 @@ import Account
|
||||
extension MasterFeedViewController: UITableViewDragDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath), identifier.isWebFeed, let url = identifier.url else {
|
||||
guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? WebFeed else {
|
||||
return [UIDragItem]()
|
||||
}
|
||||
|
||||
let data = url.data(using: .utf8)
|
||||
let data = webFeed.url.data(using: .utf8)
|
||||
let itemProvider = NSItemProvider()
|
||||
|
||||
itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .ownProcess) { completion in
|
||||
@ -26,7 +26,7 @@ extension MasterFeedViewController: UITableViewDragDelegate {
|
||||
}
|
||||
|
||||
let dragItem = UIDragItem(itemProvider: itemProvider)
|
||||
dragItem.localObject = identifier
|
||||
dragItem.localObject = node
|
||||
return [dragItem]
|
||||
}
|
||||
|
||||
|
@ -22,24 +22,22 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
guard let destIdentifier = dataSource.itemIdentifier(for: destIndexPath) else {
|
||||
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
|
||||
}
|
||||
|
||||
guard let destAccount = destIdentifier.account, let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed,
|
||||
let destAccount = destFeed.account,
|
||||
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||
return UITableViewDropProposal(operation: .forbidden)
|
||||
}
|
||||
|
||||
// Validate account specific behaviors...
|
||||
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||
let sourceFeedID = (session.localDragSession?.items.first?.localObject as? MasterFeedTableViewIdentifier)?.feedID,
|
||||
let sourceWebFeed = AccountManager.shared.existingFeed(with: sourceFeedID) as? WebFeed,
|
||||
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
||||
let sourceWebFeed = sourceNode.representedObject 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 destFeed is Folder {
|
||||
if session.location(in: destCell).y >= 0 {
|
||||
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
|
||||
} else {
|
||||
@ -53,30 +51,29 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
|
||||
guard let dragItem = dropCoordinator.items.first?.dragItem,
|
||||
let sourceIdentifier = dragItem.localObject as? MasterFeedTableViewIdentifier,
|
||||
let sourceParentContainerID = sourceIdentifier.parentContainerID,
|
||||
let source = AccountManager.shared.existingContainer(with: sourceParentContainerID),
|
||||
let destIndexPath = dropCoordinator.destinationIndexPath else {
|
||||
return
|
||||
}
|
||||
let dragNode = dragItem.localObject as? Node,
|
||||
let source = dragNode.parent?.representedObject as? Container,
|
||||
let destIndexPath = dropCoordinator.destinationIndexPath else {
|
||||
return
|
||||
}
|
||||
|
||||
let isFolderDrop: Bool = {
|
||||
if let propDestIdentifier = dataSource.itemIdentifier(for: destIndexPath), let propCell = tableView.cellForRow(at: destIndexPath) {
|
||||
return propDestIdentifier.isFolder && dropCoordinator.session.location(in: propCell).y >= 0
|
||||
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) {
|
||||
return dropCoordinator.session.location(in: propCell).y >= 0
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
// Based on the drop we have to determine a node to start looking for a parent container.
|
||||
let destIdentifier: MasterFeedTableViewIdentifier? = {
|
||||
let destNode: Node? = {
|
||||
|
||||
if isFolderDrop {
|
||||
return dataSource.itemIdentifier(for: destIndexPath)
|
||||
return coordinator.nodeFor(destIndexPath)
|
||||
} else {
|
||||
if destIndexPath.row == 0 {
|
||||
return dataSource.itemIdentifier(for: IndexPath(row: 0, section: destIndexPath.section))
|
||||
return coordinator.nodeFor(IndexPath(row: 0, section: destIndexPath.section))
|
||||
} else if destIndexPath.row > 0 {
|
||||
return dataSource.itemIdentifier(for: IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section))
|
||||
return coordinator.nodeFor(IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -86,25 +83,21 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||
|
||||
// Now we start looking for the parent container
|
||||
let destinationContainer: Container? = {
|
||||
if let containerID = destIdentifier?.containerID ?? destIdentifier?.parentContainerID {
|
||||
return AccountManager.shared.existingContainer(with: containerID)
|
||||
if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) {
|
||||
return container
|
||||
} else {
|
||||
// If we got here, we are trying to drop on an empty section header. Go and find the Account for this section
|
||||
return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account
|
||||
}
|
||||
}()
|
||||
|
||||
guard let destination = destinationContainer else { return }
|
||||
guard case .webFeed(_, let webFeedID) = sourceIdentifier.feedID else { return }
|
||||
guard let webFeed = source.existingWebFeed(withWebFeedID: webFeedID) else { return }
|
||||
guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? WebFeed else { return }
|
||||
|
||||
if source.account == destination.account {
|
||||
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
} else {
|
||||
moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
|
||||
|
@ -27,9 +27,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private let operationQueue = MainThreadOperationQueue()
|
||||
lazy var dataSource = makeDataSource()
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
@ -63,7 +60,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
tableView.tableHeaderView = UIView(frame: frame)
|
||||
|
||||
tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
tableView.dataSource = dataSource
|
||||
tableView.dragDelegate = self
|
||||
tableView.dropDelegate = self
|
||||
tableView.dragInteractionEnabled = true
|
||||
@ -83,7 +79,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
configureToolbar()
|
||||
becomeFirstResponder()
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -94,7 +89,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
IconImageCache.shared.emptyCache()
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
reloadAllVisibleCells()
|
||||
// reloadAllVisibleCells()
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
@ -123,11 +118,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
|
||||
}
|
||||
|
||||
guard let unreadCountNode = node else { return }
|
||||
let identifier = makeIdentifier(unreadCountNode)
|
||||
if dataSource.indexPath(for: identifier) != nil {
|
||||
self.reload(identifier)
|
||||
}
|
||||
guard let unreadCountNode = node, let indexPath = coordinator.indexPathFor(unreadCountNode) else { return }
|
||||
tableView.reloadRows(at: [indexPath], with: .middle)
|
||||
}
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
@ -152,7 +144,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
resetEstimatedRowHeight()
|
||||
applyChanges(animated: false)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func willEnterForeground(_ note: Notification) {
|
||||
@ -161,6 +153,20 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
coordinator.numberOfSections()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
coordinator.numberOfRows(in: section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
|
||||
configure(cell, indexPath)
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
||||
guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
@ -251,13 +257,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
renameAction.backgroundColor = UIColor.systemOrange
|
||||
actions.append(renameAction)
|
||||
|
||||
if let identifier = dataSource.itemIdentifier(for: indexPath), identifier.isWebFeed {
|
||||
if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed {
|
||||
let moreTitle = NSLocalizedString("More", comment: "More")
|
||||
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
||||
|
||||
if let self = self {
|
||||
|
||||
let alert = UIAlertController(title: identifier.nameForDisplay, message: nil, preferredStyle: .actionSheet)
|
||||
let alert = UIAlertController(title: webFeed.nameForDisplay, message: nil, preferredStyle: .actionSheet)
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1)
|
||||
@ -303,26 +309,25 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||
return nil
|
||||
}
|
||||
if identifier.isWebFeed {
|
||||
return makeWebFeedContextMenu(identifier: identifier, indexPath: indexPath, includeDeleteRename: true)
|
||||
} else if identifier.isFolder {
|
||||
return makeFolderContextMenu(identifier: identifier, indexPath: indexPath)
|
||||
} else if identifier.isPsuedoFeed {
|
||||
return makePseudoFeedContextMenu(identifier: identifier, indexPath: indexPath)
|
||||
if feed is WebFeed {
|
||||
return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||
} else if feed is Folder {
|
||||
return makeFolderContextMenu(indexPath: indexPath)
|
||||
} else if feed is PseudoFeed {
|
||||
return makePseudoFeedContextMenu(indexPath: indexPath)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
guard let identifier = configuration.identifier as? MasterFeedTableViewIdentifier,
|
||||
let indexPath = dataSource.indexPath(for: identifier),
|
||||
let cell = tableView.cellForRow(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
guard let identifier = configuration.identifier as? MasterFeedRowIdentifier,
|
||||
let cell = tableView.cellForRow(at: identifier.indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
|
||||
}
|
||||
@ -342,21 +347,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return coordinator.cappedIndexPath(proposedDestinationIndexPath)
|
||||
}()
|
||||
|
||||
guard let draggedIdentifier = dataSource.itemIdentifier(for: sourceIndexPath),
|
||||
let draggedFeedID = draggedIdentifier.feedID,
|
||||
let draggedNode = coordinator.nodeFor(feedID: draggedFeedID) else {
|
||||
guard let draggedNode = coordinator.nodeFor(sourceIndexPath) else {
|
||||
assertionFailure("This should never happen")
|
||||
return sourceIndexPath
|
||||
}
|
||||
|
||||
// If there is no destination node, we are dragging onto an empty Account
|
||||
guard let destIdentifier = dataSource.itemIdentifier(for: destIndexPath),
|
||||
let destFeedID = destIdentifier.feedID,
|
||||
let destNode = coordinator.nodeFor(feedID: destFeedID),
|
||||
let destParentContainerID = destIdentifier.parentContainerID,
|
||||
let destParentNode = coordinator.nodeFor(containerID: destParentContainerID) else {
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
guard let destNode = coordinator.nodeFor(destIndexPath),
|
||||
let destParentNode = destNode.parent else {
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
|
||||
// If this is a folder, let the users drop on it
|
||||
if destNode.representedObject is Folder {
|
||||
@ -381,8 +381,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
if destParentNode.representedObject is Account {
|
||||
return IndexPath(row: 0, section: destIndexPath.section)
|
||||
} else {
|
||||
let identifier = makeIdentifier(sortedNodes[index])
|
||||
if let candidateIndexPath = dataSource.indexPath(for: identifier) {
|
||||
if let candidateIndexPath = coordinator.indexPathFor(sortedNodes[index]) {
|
||||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
return IndexPath(row: candidateIndexPath.row - movementAdjustment, section: candidateIndexPath.section)
|
||||
} else {
|
||||
@ -393,8 +392,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
} else {
|
||||
|
||||
if index >= sortedNodes.count {
|
||||
let identifier = makeIdentifier(sortedNodes[sortedNodes.count - 1])
|
||||
if let lastSortedIndexPath = dataSource.indexPath(for: identifier) {
|
||||
if let lastSortedIndexPath = coordinator.indexPathFor(sortedNodes[sortedNodes.count - 1]) {
|
||||
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
|
||||
return IndexPath(row: lastSortedIndexPath.row + movementAdjustment, section: lastSortedIndexPath.section)
|
||||
} else {
|
||||
@ -402,8 +400,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
} else {
|
||||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
let identifer = makeIdentifier(sortedNodes[index - movementAdjustment])
|
||||
return dataSource.indexPath(for: identifer) ?? sourceIndexPath
|
||||
return coordinator.indexPathFor(sortedNodes[index - movementAdjustment]) ?? sourceIndexPath
|
||||
}
|
||||
|
||||
}
|
||||
@ -515,14 +512,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
@objc func expandSelectedRows(_ sender: Any?) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID {
|
||||
coordinator.expand(containerID)
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) {
|
||||
coordinator.expand(node)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func collapseSelectedRows(_ sender: Any?) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID {
|
||||
coordinator.collapse(containerID)
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) {
|
||||
coordinator.collapse(node)
|
||||
}
|
||||
}
|
||||
|
||||
@ -562,23 +559,54 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
func updateFeedSelection(animations: Animations) {
|
||||
operationQueue.add(UpdateSelectionOperation(coordinator: coordinator, dataSource: dataSource, tableView: tableView, animations: animations))
|
||||
}
|
||||
|
||||
func reloadFeeds(initialLoad: Bool, completion: (() -> Void)? = nil) {
|
||||
updateUI()
|
||||
|
||||
// We have to reload all the visible cells because if we got here by doing a table cell move,
|
||||
// then the table itself is in a weird state. This is because we do unusual things like allowing
|
||||
// drops on a "folder" that should cause the dropped cell to disappear.
|
||||
applyChanges(animated: !initialLoad) { [weak self] in
|
||||
if !initialLoad {
|
||||
self?.reloadAllVisibleCells(completion: completion)
|
||||
} else {
|
||||
completion?()
|
||||
if let indexPath = coordinator.currentFeedIndexPath {
|
||||
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
|
||||
} else {
|
||||
if let indexPath = tableView.indexPathForSelectedRow {
|
||||
if animations.contains(.select) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
} else {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadFeeds(initialLoad: Bool, changes: [ShadowTableChanges], completion: (() -> Void)? = nil) {
|
||||
updateUI()
|
||||
|
||||
guard !initialLoad else {
|
||||
tableView.reloadData()
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
for change in changes {
|
||||
guard !change.isEmpty else { continue }
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
if let deletes = change.deleteIndexPaths, !deletes.isEmpty {
|
||||
tableView.deleteRows(at: deletes, with: .middle)
|
||||
}
|
||||
|
||||
if let inserts = change.insertIndexPaths, !inserts.isEmpty {
|
||||
tableView.insertRows(at: inserts, with: .middle)
|
||||
}
|
||||
|
||||
if let moves = change.moveIndexPaths, !moves.isEmpty {
|
||||
for move in moves {
|
||||
tableView.moveRow(at: move.0, to: move.1)
|
||||
}
|
||||
}
|
||||
|
||||
if let reloads = change.reloadIndexPaths, !reloads.isEmpty {
|
||||
tableView.reloadRows(at: reloads, with: .middle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
func updateUI() {
|
||||
if coordinator.isReadFeedsFiltered {
|
||||
@ -741,65 +769,6 @@ private extension MasterFeedViewController {
|
||||
filterButton?.accLabelText = NSLocalizedString("Filter Read Feeds", comment: "Filter Read Feeds")
|
||||
}
|
||||
|
||||
func makeIdentifier(_ node: Node) -> MasterFeedTableViewIdentifier {
|
||||
let unreadCount = coordinator.unreadCountFor(node)
|
||||
return MasterFeedTableViewIdentifier(node: node, unreadCount: unreadCount)
|
||||
}
|
||||
|
||||
func reload(_ identifier: MasterFeedTableViewIdentifier) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems([identifier])
|
||||
queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
||||
}
|
||||
}
|
||||
|
||||
func applyChanges(animated: Bool, adjustScroll: Bool = false, completion: (() -> Void)? = nil) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, MasterFeedTableViewIdentifier>()
|
||||
let sectionIdentifiers = Array(0...coordinator.rootNode.childNodes.count - 1)
|
||||
snapshot.appendSections(sectionIdentifiers)
|
||||
|
||||
for sectionIdentifer in sectionIdentifiers {
|
||||
let identifiers = coordinator.shadowNodesFor(section: sectionIdentifer).map { makeIdentifier($0) }
|
||||
snapshot.appendItems(identifiers, toSection: sectionIdentifer)
|
||||
}
|
||||
|
||||
queueApply(snapshot: snapshot, animatingDifferences: animated) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary(adjustScroll: adjustScroll)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
func queueApply(snapshot: NSDiffableDataSourceSnapshot<Int, MasterFeedTableViewIdentifier>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) {
|
||||
let operation = MasterFeedDataSourceOperation(dataSource: dataSource, snapshot: snapshot, animating: animatingDifferences)
|
||||
operation.completionBlock = { [weak self] _ in
|
||||
self?.enableTableViewSelection()
|
||||
completion?()
|
||||
}
|
||||
disableTableViewSelectionIfNecessary()
|
||||
operationQueue.add(operation)
|
||||
}
|
||||
|
||||
private func disableTableViewSelectionIfNecessary() {
|
||||
// We only need to disable tableView selection if the feeds are filtered by unread
|
||||
guard coordinator.isReadFeedsFiltered else { return }
|
||||
tableView.allowsSelection = false
|
||||
}
|
||||
|
||||
private func enableTableViewSelection() {
|
||||
tableView.allowsSelection = true
|
||||
}
|
||||
|
||||
func makeDataSource() -> MasterFeedDataSource {
|
||||
let dataSource = MasterFeedDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, cellContents in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
|
||||
self?.configure(cell, cellContents)
|
||||
return cell
|
||||
})
|
||||
dataSource.defaultRowAnimation = .middle
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func resetEstimatedRowHeight() {
|
||||
let titleLabel = NonIntrinsicLabel()
|
||||
titleLabel.text = "But I must explain"
|
||||
@ -811,27 +780,30 @@ private extension MasterFeedViewController {
|
||||
tableView.estimatedRowHeight = layout.height
|
||||
}
|
||||
|
||||
func configure(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
|
||||
|
||||
func configure(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
|
||||
guard let node = coordinator.nodeFor(indexPath) else { return }
|
||||
|
||||
cell.delegate = self
|
||||
if identifier.isFolder {
|
||||
if node.representedObject is Folder {
|
||||
cell.indentationLevel = 0
|
||||
} else {
|
||||
cell.indentationLevel = 1
|
||||
}
|
||||
|
||||
if let containerID = identifier.containerID {
|
||||
if let containerID = (node.representedObject as? Container)?.containerID {
|
||||
cell.setDisclosure(isExpanded: coordinator.isExpanded(containerID), animated: false)
|
||||
cell.isDisclosureAvailable = true
|
||||
} else {
|
||||
cell.isDisclosureAvailable = false
|
||||
}
|
||||
|
||||
cell.name = identifier.nameForDisplay
|
||||
cell.unreadCount = identifier.unreadCount
|
||||
configureIcon(cell, identifier)
|
||||
|
||||
guard let indexPath = dataSource.indexPath(for: identifier) else { return }
|
||||
if let feed = node.representedObject as? Feed {
|
||||
cell.name = feed.nameForDisplay
|
||||
cell.unreadCount = feed.unreadCount
|
||||
}
|
||||
|
||||
configureIcon(cell, indexPath)
|
||||
|
||||
let rowsInSection = tableView.numberOfRows(inSection: indexPath.section)
|
||||
if indexPath.row == rowsInSection - 1 {
|
||||
cell.isSeparatorShown = false
|
||||
@ -841,8 +813,8 @@ private extension MasterFeedViewController {
|
||||
|
||||
}
|
||||
|
||||
func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) {
|
||||
guard let feedID = identifier.feedID else {
|
||||
func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
|
||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed, let feedID = feed.feedID else {
|
||||
return
|
||||
}
|
||||
cell.iconImage = IconImageCache.shared.imageFor(feedID)
|
||||
@ -859,35 +831,29 @@ private extension MasterFeedViewController {
|
||||
applyToCellsForRepresentedObject(representedObject, configure)
|
||||
}
|
||||
|
||||
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, MasterFeedTableViewIdentifier) -> Void) {
|
||||
applyToAvailableCells { (cell, identifier) in
|
||||
if let representedFeed = representedObject as? Feed, representedFeed.feedID == identifier.feedID {
|
||||
completion(cell, identifier)
|
||||
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
|
||||
applyToAvailableCells { (cell, indexPath) in
|
||||
if let node = coordinator.nodeFor(indexPath),
|
||||
let representedFeed = representedObject as? Feed,
|
||||
let candidate = node.representedObject as? Feed,
|
||||
representedFeed.feedID == candidate.feedID {
|
||||
completion(cell, indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyToAvailableCells(_ completion: (MasterFeedTableViewCell, MasterFeedTableViewIdentifier) -> Void) {
|
||||
func applyToAvailableCells(_ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
|
||||
tableView.visibleCells.forEach { cell in
|
||||
guard let indexPath = tableView.indexPath(for: cell), let identifier = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
return
|
||||
}
|
||||
completion(cell as! MasterFeedTableViewCell, identifier)
|
||||
completion(cell as! MasterFeedTableViewCell, indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadAllVisibleCells(completion: (() -> Void)? = nil) {
|
||||
let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
|
||||
reloadCells(visibleNodes, completion: completion)
|
||||
}
|
||||
|
||||
private func reloadCells(_ identifiers: [MasterFeedTableViewIdentifier], completion: (() -> Void)? = nil) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems(identifiers)
|
||||
queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in
|
||||
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
||||
completion?()
|
||||
}
|
||||
guard let indexPaths = tableView.indexPathsForVisibleRows else { return }
|
||||
tableView.reloadRows(at: indexPaths, with: .middle)
|
||||
}
|
||||
|
||||
private func accountForNode(_ node: Node) -> Account? {
|
||||
@ -918,21 +884,21 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func expand(_ cell: MasterFeedTableViewCell) {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID else {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else {
|
||||
return
|
||||
}
|
||||
coordinator.expand(containerID)
|
||||
coordinator.expand(node)
|
||||
}
|
||||
|
||||
func collapse(_ cell: MasterFeedTableViewCell) {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID else {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else {
|
||||
return
|
||||
}
|
||||
coordinator.collapse(containerID)
|
||||
coordinator.collapse(node)
|
||||
}
|
||||
|
||||
func makeWebFeedContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
func makeWebFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
@ -976,8 +942,8 @@ private extension MasterFeedViewController {
|
||||
|
||||
}
|
||||
|
||||
func makeFolderContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
@ -999,12 +965,12 @@ private extension MasterFeedViewController {
|
||||
})
|
||||
}
|
||||
|
||||
func makePseudoFeedContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath) -> UIContextMenuConfiguration? {
|
||||
func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration? {
|
||||
guard let markAllAction = self.markAllAsReadAction(indexPath: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { suggestedActions in
|
||||
return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in
|
||||
return UIMenu(title: "", children: [markAllAction])
|
||||
})
|
||||
}
|
||||
@ -1035,11 +1001,10 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
|
||||
let action = UIAction(title: title, image: AppAssets.copyImage) { action in
|
||||
@ -1049,12 +1014,11 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let url = URL(string: webFeed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
|
||||
let action = UIAlertAction(title: title, style: .default) { action in
|
||||
UIPasteboard.general.url = url
|
||||
@ -1064,12 +1028,11 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL")
|
||||
let action = UIAction(title: title, image: AppAssets.copyImage) { action in
|
||||
@ -1079,13 +1042,12 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID,
|
||||
let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
let homePageURL = webFeed.homePageURL,
|
||||
let url = URL(string: homePageURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL")
|
||||
let action = UIAlertAction(title: title, style: .default) { action in
|
||||
UIPasteboard.general.url = url
|
||||
@ -1095,16 +1057,14 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath),
|
||||
identifier.unreadCount > 0,
|
||||
let feedID = identifier.feedID,
|
||||
let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed,
|
||||
let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
||||
webFeed.unreadCount > 0,
|
||||
let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
@ -1137,13 +1097,13 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func getInfoAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed else {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Get Info", comment: "Get Info")
|
||||
let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in
|
||||
self?.coordinator.showFeedInspector(for: feed)
|
||||
self?.coordinator.showFeedInspector(for: webFeed)
|
||||
}
|
||||
return action
|
||||
}
|
||||
@ -1165,41 +1125,25 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed else {
|
||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = NSLocalizedString("Get Info", comment: "Get Info")
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
self?.coordinator.showFeedInspector(for: feed)
|
||||
self?.coordinator.showFeedInspector(for: webFeed)
|
||||
completion(true)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func markAllAsReadAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let identifier = dataSource.itemIdentifier(for: indexPath), identifier.unreadCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView,
|
||||
feed.unreadCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var smartFeed: Feed?
|
||||
if identifier.isPsuedoFeed {
|
||||
if SmartFeedsController.shared.todayFeed.feedID == identifier.feedID {
|
||||
smartFeed = SmartFeedsController.shared.todayFeed
|
||||
} else if SmartFeedsController.shared.unreadFeed.feedID == identifier.feedID {
|
||||
smartFeed = SmartFeedsController.shared.unreadFeed
|
||||
} else if SmartFeedsController.shared.starredFeed.feedID == identifier.feedID {
|
||||
smartFeed = SmartFeedsController.shared.starredFeed
|
||||
}
|
||||
}
|
||||
|
||||
guard let feedID = identifier.feedID,
|
||||
let feed = smartFeed ?? AccountManager.shared.existingFeed(with: feedID),
|
||||
feed.unreadCount > 0,
|
||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
@ -1236,11 +1180,10 @@ private extension MasterFeedViewController {
|
||||
|
||||
|
||||
func rename(indexPath: IndexPath) {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) else { return }
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
||||
|
||||
let name = dataSource.itemIdentifier(for: indexPath)?.nameForDisplay ?? ""
|
||||
let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed")
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String
|
||||
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
|
||||
@ -1280,7 +1223,7 @@ private extension MasterFeedViewController {
|
||||
alertController.preferredAction = renameAction
|
||||
|
||||
alertController.addTextField() { textField in
|
||||
textField.text = name
|
||||
textField.text = feed.nameForDisplay
|
||||
textField.placeholder = NSLocalizedString("Name", comment: "Name")
|
||||
}
|
||||
|
||||
@ -1291,7 +1234,7 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
func delete(indexPath: IndexPath) {
|
||||
guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) else { return }
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
||||
|
||||
let title: String
|
||||
let message: String
|
||||
@ -1312,7 +1255,7 @@ private extension MasterFeedViewController {
|
||||
|
||||
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
|
||||
let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] action in
|
||||
self?.delete(indexPath: indexPath, feedID: feedID)
|
||||
self?.performDelete(indexPath: indexPath)
|
||||
}
|
||||
alertController.addAction(deleteAction)
|
||||
alertController.preferredAction = deleteAction
|
||||
@ -1320,9 +1263,9 @@ private extension MasterFeedViewController {
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
func delete(indexPath: IndexPath, feedID: FeedIdentifier) {
|
||||
func performDelete(indexPath: IndexPath) {
|
||||
guard let undoManager = undoManager,
|
||||
let deleteNode = coordinator.nodeFor(feedID: feedID),
|
||||
let deleteNode = coordinator.nodeFor(indexPath),
|
||||
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], undoManager: undoManager, errorHandler: ErrorHandler.present(self)) else {
|
||||
return
|
||||
}
|
||||
|
@ -688,7 +688,7 @@ private extension MasterTimelineViewController {
|
||||
|
||||
func updateTitleUnreadCount() {
|
||||
if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
|
||||
titleView.unreadCountView.unreadCount = coordinator.unreadCount
|
||||
titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ enum PanelMode {
|
||||
case three
|
||||
case standard
|
||||
}
|
||||
|
||||
enum SearchScope: Int {
|
||||
case timeline = 0
|
||||
case global = 1
|
||||
@ -30,7 +31,21 @@ enum ShowFeedName {
|
||||
case feed
|
||||
}
|
||||
|
||||
class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
struct FeedNode: Hashable {
|
||||
var node: Node
|
||||
var feedID: FeedIdentifier
|
||||
|
||||
init(_ node: Node) {
|
||||
self.node = node
|
||||
self.feedID = (node.representedObject as! Feed).feedID!
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(feedID)
|
||||
}
|
||||
}
|
||||
|
||||
class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
var undoManager: UndoManager? {
|
||||
@ -74,7 +89,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
private var expandedTable = Set<ContainerIdentifier>()
|
||||
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
||||
private var shadowTable = [[Node]]()
|
||||
private var shadowTable = [[FeedNode]]()
|
||||
|
||||
private(set) var preSearchTimelineFeed: Feed?
|
||||
private var lastSearchString = ""
|
||||
@ -285,20 +300,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
}
|
||||
|
||||
var isTimelineUnreadAvailable: Bool {
|
||||
return unreadCount > 0
|
||||
return timelineUnreadCount > 0
|
||||
}
|
||||
|
||||
var isAnyUnreadAvailable: Bool {
|
||||
return appDelegate.unreadCount > 0
|
||||
}
|
||||
|
||||
var unreadCount: Int = 0 {
|
||||
didSet {
|
||||
if unreadCount != oldValue {
|
||||
postUnreadCountDidChangeNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
var timelineUnreadCount: Int = 0
|
||||
|
||||
override init() {
|
||||
treeController = TreeController(delegate: treeControllerDelegate)
|
||||
@ -307,7 +316,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
for sectionNode in treeController.rootNode.childNodes {
|
||||
markExpanded(sectionNode)
|
||||
shadowTable.append([Node]())
|
||||
shadowTable.append([FeedNode]())
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
|
||||
@ -650,20 +659,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
refreshTimeline(resetScroll: false)
|
||||
}
|
||||
|
||||
func shadowNodesFor(section: Int) -> [Node] {
|
||||
return shadowTable[section]
|
||||
}
|
||||
|
||||
func nodeFor(containerID: ContainerIdentifier) -> Node? {
|
||||
return treeController.rootNode.descendantNode(where: { node in
|
||||
if let container = node.representedObject as? Container {
|
||||
return container.containerID == containerID
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func nodeFor(feedID: FeedIdentifier) -> Node? {
|
||||
return treeController.rootNode.descendantNode(where: { node in
|
||||
@ -675,6 +670,30 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
})
|
||||
}
|
||||
|
||||
func numberOfSections() -> Int {
|
||||
return shadowTable.count
|
||||
}
|
||||
|
||||
func numberOfRows(in section: Int) -> Int {
|
||||
return shadowTable[section].count
|
||||
}
|
||||
|
||||
func nodeFor(_ indexPath: IndexPath) -> Node? {
|
||||
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
|
||||
return nil
|
||||
}
|
||||
return shadowTable[indexPath.section][indexPath.row].node
|
||||
}
|
||||
|
||||
func indexPathFor(_ node: Node) -> IndexPath? {
|
||||
for i in 0..<shadowTable.count {
|
||||
if let row = shadowTable[i].firstIndex(of: FeedNode(node)) {
|
||||
return IndexPath(row: row, section: i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func articleFor(_ articleID: String) -> Article? {
|
||||
return idToAticleDictionary[articleID]
|
||||
}
|
||||
@ -689,7 +708,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
func unreadCountFor(_ node: Node) -> Int {
|
||||
// The coordinator supplies the unread count for the currently selected feed
|
||||
if node.representedObject === timelineFeed as AnyObject {
|
||||
return unreadCount
|
||||
return timelineUnreadCount
|
||||
}
|
||||
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
|
||||
return unreadCountProvider.unreadCount
|
||||
@ -1430,7 +1449,7 @@ private extension SceneCoordinator {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
unreadCount = count
|
||||
timelineUnreadCount = count
|
||||
}
|
||||
|
||||
func rebuildArticleDictionaries() {
|
||||
@ -1483,8 +1502,8 @@ private extension SceneCoordinator {
|
||||
|
||||
func addShadowTableToFilterExceptions() {
|
||||
for section in shadowTable {
|
||||
for node in section {
|
||||
if let feed = node.representedObject as? Feed, let feedID = feed.feedID {
|
||||
for feedNode in section {
|
||||
if let feed = feedNode.node.representedObject as? Feed, let feedID = feed.feedID {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
}
|
||||
}
|
||||
@ -1501,51 +1520,81 @@ private extension SceneCoordinator {
|
||||
|
||||
func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil, completion: (() -> Void)? = nil) {
|
||||
if !BatchUpdate.shared.isPerforming {
|
||||
|
||||
addToFilterExeptionsIfNecessary(timelineFeed)
|
||||
treeController.rebuild()
|
||||
treeControllerDelegate.resetFilterExceptions()
|
||||
|
||||
updateExpandedNodes?()
|
||||
rebuildShadowTable()
|
||||
masterFeedViewController.reloadFeeds(initialLoad: initialLoad, completion: completion)
|
||||
|
||||
let changes = rebuildShadowTable()
|
||||
masterFeedViewController.reloadFeeds(initialLoad: initialLoad, changes: changes, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func rebuildShadowTable() {
|
||||
shadowTable = [[Node]]()
|
||||
func rebuildShadowTable() -> [ShadowTableChanges] {
|
||||
var newShadowTable = [[FeedNode]]()
|
||||
|
||||
for i in 0..<treeController.rootNode.numberOfChildNodes {
|
||||
|
||||
var result = [Node]()
|
||||
var result = [FeedNode]()
|
||||
let sectionNode = treeController.rootNode.childAtIndex(i)!
|
||||
|
||||
if isExpanded(sectionNode) {
|
||||
for node in sectionNode.childNodes {
|
||||
result.append(node)
|
||||
result.append(FeedNode(node))
|
||||
if isExpanded(node) {
|
||||
for child in node.childNodes {
|
||||
result.append(child)
|
||||
result.append(FeedNode(child))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shadowTable.append(result)
|
||||
|
||||
newShadowTable.append(result)
|
||||
}
|
||||
|
||||
// If we have a current Feed IndexPath it is no longer valid and needs reset.
|
||||
if currentFeedIndexPath != nil {
|
||||
currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject)
|
||||
}
|
||||
|
||||
// Compute the differences in the shadow tables
|
||||
var changes = [ShadowTableChanges]()
|
||||
|
||||
for (index, newSectionNodes) in newShadowTable.enumerated() {
|
||||
var moves = Set<ShadowTableChanges.Move>()
|
||||
var inserts = Set<Int>()
|
||||
var deletes = Set<Int>()
|
||||
|
||||
let diff = newSectionNodes.difference(from: shadowTable[index]).inferringMoves()
|
||||
for change in diff {
|
||||
switch change {
|
||||
case .insert(let offset, _, let associated):
|
||||
if let associated = associated {
|
||||
moves.insert(ShadowTableChanges.Move(associated, offset))
|
||||
} else {
|
||||
inserts.insert(offset)
|
||||
}
|
||||
case .remove(let offset, _, let associated):
|
||||
if let associated = associated {
|
||||
moves.insert(ShadowTableChanges.Move(offset, associated))
|
||||
} else {
|
||||
deletes.insert(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes.append(ShadowTableChanges(section: index, deletes: deletes, inserts: inserts, moves: moves))
|
||||
}
|
||||
|
||||
shadowTable = newShadowTable
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
func shadowTableContains(_ feed: Feed) -> Bool {
|
||||
for section in shadowTable {
|
||||
for node in section {
|
||||
if let nodeFeed = node.representedObject as? Feed, nodeFeed.feedID == feed.feedID {
|
||||
for feedNode in section {
|
||||
if let nodeFeed = feedNode.node.representedObject as? Feed, nodeFeed.feedID == feed.feedID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -1559,22 +1608,6 @@ private extension SceneCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
func nodeFor(_ indexPath: IndexPath) -> Node? {
|
||||
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
|
||||
return nil
|
||||
}
|
||||
return shadowTable[indexPath.section][indexPath.row]
|
||||
}
|
||||
|
||||
func indexPathFor(_ node: Node) -> IndexPath? {
|
||||
for i in 0..<shadowTable.count {
|
||||
if let row = shadowTable[i].firstIndex(of: node) {
|
||||
return IndexPath(row: row, section: i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexPathFor(_ object: AnyObject) -> IndexPath? {
|
||||
guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
|
||||
return nil
|
||||
|
65
iOS/ShadowTableChanges.swift
Normal file
65
iOS/ShadowTableChanges.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// ShadowTableChanges.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 10/20/21.
|
||||
// Copyright © 2021 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ShadowTableChanges {
|
||||
|
||||
public struct Move: Hashable {
|
||||
public var from: Int
|
||||
public var to: Int
|
||||
|
||||
init(_ from: Int, _ to: Int) {
|
||||
self.from = from
|
||||
self.to = to
|
||||
}
|
||||
}
|
||||
|
||||
public var section: Int
|
||||
public var deletes: Set<Int>?
|
||||
public var inserts: Set<Int>?
|
||||
public var moves: Set<Move>?
|
||||
public var reloads: Set<Int>?
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true) && (reloads?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
public var isOnlyReloads: Bool {
|
||||
return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
public var deleteIndexPaths: [IndexPath]? {
|
||||
guard let deletes = deletes else { return nil }
|
||||
return deletes.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
public var insertIndexPaths: [IndexPath]? {
|
||||
guard let inserts = inserts else { return nil }
|
||||
return inserts.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
public var moveIndexPaths: [(IndexPath, IndexPath)]? {
|
||||
guard let moves = moves else { return nil }
|
||||
return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) }
|
||||
}
|
||||
|
||||
public var reloadIndexPaths: [IndexPath]? {
|
||||
guard let reloads = reloads else { return nil }
|
||||
return reloads.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
init(section: Int, deletes: Set<Int>? = nil, inserts: Set<Int>? = nil, moves: Set<Move>? = nil, reloads: Set<Int>? = nil) {
|
||||
self.section = section
|
||||
self.deletes = deletes
|
||||
self.inserts = inserts
|
||||
self.moves = moves
|
||||
self.reloads = reloads
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user