Reload cells when HTMLMetadata is available.
This commit is contained in:
parent
703ecfa746
commit
9f5fc9093a
@ -11,6 +11,7 @@ import Account
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSTree
|
||||
import RSWeb
|
||||
import SafariServices
|
||||
import WebKit
|
||||
|
||||
@ -26,18 +27,18 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
|
||||
private let keyboardManager = KeyboardManager(type: .sidebar)
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
|
||||
|
||||
// If the first responder is the WKWebView we don't want to supply any keyboard
|
||||
// commands that the system is looking for by going up the responder chain. They will interfere with
|
||||
// the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys.
|
||||
guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil }
|
||||
|
||||
|
||||
return keyboardManager.keyCommands
|
||||
}
|
||||
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
@ -49,13 +50,13 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
if traitCollection.userInterfaceIdiom == .phone {
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
}
|
||||
|
||||
|
||||
// If you don't have an empty table header, UIKit tries to help out by putting one in for you
|
||||
// that makes a gap between the first section header and the navigation bar
|
||||
var frame = CGRect.zero
|
||||
frame.size.height = .leastNormalMagnitude
|
||||
tableView.tableHeaderView = UIView(frame: frame)
|
||||
|
||||
|
||||
tableView.register(MainFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
tableView.dragDelegate = self
|
||||
tableView.dropDelegate = self
|
||||
@ -65,6 +66,13 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
|
||||
// TODO: fix this temporary hack, which will probably require refactoring image handling.
|
||||
// We want to know when to possibly reconfigure our cells with a new image, and we don’t
|
||||
// always know when an image is available — but watching the .htmlMetadataAvailable Notification
|
||||
// lets us know that it’s time to request an image.
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .htmlMetadataAvailable, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .feedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
@ -81,7 +89,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
navigationController?.isToolbarHidden = false
|
||||
navigationController?.isToolbarHidden = false
|
||||
updateUI()
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
@ -109,7 +117,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
guard let unreadCountProvider = note.object as? UnreadCountProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let account = unreadCountProvider as? Account {
|
||||
if let headerView = headerViewForAccount(account) {
|
||||
headerView.unreadCount = account.unreadCount
|
||||
@ -168,27 +176,27 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
resetEstimatedRowHeight()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
|
||||
@objc func willEnterForeground(_ note: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
|
||||
// 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! MainFeedTableViewCell
|
||||
configure(cell, indexPath)
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
if coordinator.nodeFor(indexPath)?.representedObject is PseudoFeed {
|
||||
return false
|
||||
@ -196,44 +204,44 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
||||
guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
return 44
|
||||
}
|
||||
|
||||
|
||||
let headerView = MainFeedTableViewSectionHeader()
|
||||
headerView.name = nameProvider.nameForDisplay
|
||||
|
||||
let size = headerView.sizeThatFits(CGSize(width: tableView.bounds.width, height: 0.0))
|
||||
return size.height
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
|
||||
|
||||
guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! MainFeedTableViewSectionHeader
|
||||
headerView.delegate = self
|
||||
headerView.name = nameProvider.nameForDisplay
|
||||
|
||||
|
||||
guard let sectionNode = coordinator.rootNode.childAtIndex(section) else {
|
||||
return headerView
|
||||
}
|
||||
|
||||
|
||||
if let account = sectionNode.representedObject as? Account {
|
||||
headerView.unreadCount = account.unreadCount
|
||||
} else {
|
||||
headerView.unreadCount = 0
|
||||
}
|
||||
|
||||
|
||||
headerView.tag = section
|
||||
headerView.disclosureExpanded = coordinator.isExpanded(sectionNode)
|
||||
|
||||
|
||||
if section == tableView.numberOfSections - 1 {
|
||||
headerView.isLastSection = true
|
||||
} else {
|
||||
@ -243,7 +251,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
headerView.gestureRecognizers?.removeAll()
|
||||
let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:)))
|
||||
headerView.addGestureRecognizer(tap)
|
||||
|
||||
|
||||
// Without this the swipe gesture registers on the cell below
|
||||
let gestureRecognizer = UIPanGestureRecognizer(target: nil, action: nil)
|
||||
gestureRecognizer.delegate = self
|
||||
@ -253,11 +261,11 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
if section != 0 {
|
||||
headerView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
}
|
||||
|
||||
|
||||
return headerView
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||
return CGFloat.leastNormalMagnitude
|
||||
}
|
||||
@ -268,7 +276,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
var actions = [UIContextualAction]()
|
||||
|
||||
|
||||
// Set up the delete action
|
||||
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
|
||||
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in
|
||||
@ -277,7 +285,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
deleteAction.backgroundColor = UIColor.systemRed
|
||||
actions.append(deleteAction)
|
||||
|
||||
|
||||
// Set up the rename action
|
||||
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
|
||||
let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (action, view, completion) in
|
||||
@ -286,27 +294,27 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
renameAction.backgroundColor = UIColor.systemOrange
|
||||
actions.append(renameAction)
|
||||
|
||||
|
||||
if let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed {
|
||||
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: feed.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)
|
||||
}
|
||||
|
||||
|
||||
if let action = self.getInfoAlertAction(indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
|
||||
if let action = self.homePageAlertAction(indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
|
||||
if let action = self.copyFeedPageAlertAction(indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
@ -314,30 +322,30 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
if let action = self.copyHomePageAlertAction(indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
|
||||
if let action = self.markAllAsReadAlertAction(indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in
|
||||
completion(true)
|
||||
})
|
||||
|
||||
self.present(alert, animated: true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
moreAction.backgroundColor = UIColor.systemGray
|
||||
actions.append(moreAction)
|
||||
}
|
||||
|
||||
return UISwipeActionsConfiguration(actions: actions)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else {
|
||||
return nil
|
||||
@ -352,7 +360,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
guard let identifier = configuration.identifier as? MainFeedRowIdentifier,
|
||||
let cell = tableView.cellForRow(at: identifier.indexPath) else {
|
||||
@ -376,28 +384,28 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
return coordinator.cappedIndexPath(proposedDestinationIndexPath)
|
||||
}()
|
||||
|
||||
|
||||
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 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 {
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
|
||||
|
||||
// If we are dragging around in the same container, just return the original source
|
||||
if destParentNode.childNodes.contains(draggedNode) {
|
||||
return sourceIndexPath
|
||||
}
|
||||
|
||||
|
||||
// Suggest to the user the best place to drop the feed
|
||||
// Revisit if the tree controller can ever be sorted in some other way.
|
||||
let nodes = destParentNode.childNodes + [draggedNode]
|
||||
@ -407,7 +415,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
sortedNodes.remove(at: index)
|
||||
|
||||
if index == 0 {
|
||||
|
||||
|
||||
if destParentNode.representedObject is Account {
|
||||
return IndexPath(row: 0, section: destIndexPath.section)
|
||||
} else {
|
||||
@ -418,9 +426,9 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return sourceIndexPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
if index >= sortedNodes.count {
|
||||
if let lastSortedIndexPath = coordinator.indexPathFor(sortedNodes[sortedNodes.count - 1]) {
|
||||
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
|
||||
@ -432,43 +440,43 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
return coordinator.indexPathFor(sortedNodes[index - movementAdjustment]) ?? sourceIndexPath
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@IBAction func settings(_ sender: UIBarButtonItem) {
|
||||
coordinator.showSettings()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleFilter(_ sender: Any) {
|
||||
coordinator.toggleReadFeedsFilter()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func add(_ sender: UIBarButtonItem) {
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) {
|
||||
guard let headerView = sender.view as? MainFeedTableViewSectionHeader else {
|
||||
return
|
||||
}
|
||||
toggle(headerView)
|
||||
}
|
||||
|
||||
|
||||
@objc func refreshAccounts(_ sender: Any) {
|
||||
refreshControl?.endRefreshing()
|
||||
|
||||
|
||||
// This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl.
|
||||
// If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Keyboard shortcuts
|
||||
|
||||
|
||||
@objc func selectNextUp(_ sender: Any?) {
|
||||
coordinator.selectPrevFeed()
|
||||
}
|
||||
@ -484,13 +492,13 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
@objc func openInBrowser(_ sender: Any?) {
|
||||
coordinator.showBrowserForCurrentFeed()
|
||||
}
|
||||
|
||||
|
||||
@objc override func delete(_ sender: Any?) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath {
|
||||
delete(indexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if action == #selector(UIResponder.delete(_:)) {
|
||||
return isFirstResponder
|
||||
@ -503,17 +511,17 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
coordinator.expand(node)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func collapseSelectedRows(_ sender: Any?) {
|
||||
if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) {
|
||||
coordinator.collapse(node)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func expandAll(_ sender: Any?) {
|
||||
coordinator.expandAllSectionsAndFolders()
|
||||
}
|
||||
|
||||
|
||||
@objc func collapseAllExceptForGroupItems(_ sender: Any?) {
|
||||
coordinator.collapseAllFolders()
|
||||
}
|
||||
@ -528,13 +536,13 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
self?.coordinator.markAllAsReadInTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func showFeedInspector(_ sender: Any?) {
|
||||
coordinator.showFeedInspector()
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
|
||||
func restoreSelectionIfNecessary(adjustScroll: Bool) {
|
||||
if let indexPath = coordinator.mainFeedIndexPathForCurrentTimeline() {
|
||||
if adjustScroll {
|
||||
@ -567,16 +575,16 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
if let deletes = changes.deletes, !deletes.isEmpty {
|
||||
tableView.deleteSections(IndexSet(deletes), with: .middle)
|
||||
}
|
||||
|
||||
|
||||
if let inserts = changes.inserts, !inserts.isEmpty {
|
||||
tableView.insertSections(IndexSet(inserts), with: .middle)
|
||||
}
|
||||
|
||||
|
||||
if let moves = changes.moves, !moves.isEmpty {
|
||||
for move in moves {
|
||||
tableView.moveSection(move.from, toSection: move.to)
|
||||
@ -588,11 +596,11 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
if let deletes = rowChange.deleteIndexPaths, !deletes.isEmpty {
|
||||
tableView.deleteRows(at: deletes, with: .middle)
|
||||
}
|
||||
|
||||
|
||||
if let inserts = rowChange.insertIndexPaths, !inserts.isEmpty {
|
||||
tableView.insertRows(at: inserts, with: .middle)
|
||||
}
|
||||
|
||||
|
||||
if let moves = rowChange.moveIndexPaths, !moves.isEmpty {
|
||||
for move in moves {
|
||||
tableView.moveRow(at: move.0, to: move.1)
|
||||
@ -601,7 +609,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let rowChanges = changes.rowChanges {
|
||||
for rowChange in rowChanges {
|
||||
if let reloads = rowChange.reloadIndexPaths, !reloads.isEmpty {
|
||||
@ -612,7 +620,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
|
||||
func updateUI() {
|
||||
if coordinator.isReadFeedsFiltered {
|
||||
setFilterButtonToActive()
|
||||
@ -624,7 +632,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
configureContextMenu()
|
||||
}
|
||||
|
||||
|
||||
@objc
|
||||
func configureContextMenu(_: Any? = nil) {
|
||||
/*
|
||||
@ -677,7 +685,7 @@ extension MainFeedViewController: UIContextMenuInteractionDelegate {
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { suggestedActions in
|
||||
|
||||
var menuElements = [UIMenuElement]()
|
||||
@ -688,18 +696,18 @@ extension MainFeedViewController: UIContextMenuInteractionDelegate {
|
||||
}
|
||||
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.deactivateAccountAction(account: account)]))
|
||||
|
||||
|
||||
return UIMenu(title: "", children: menuElements)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
|
||||
|
||||
guard let sectionIndex = configuration.identifier as? Int,
|
||||
let cell = tableView.headerView(forSection: sectionIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
|
||||
}
|
||||
}
|
||||
@ -707,17 +715,17 @@ extension MainFeedViewController: UIContextMenuInteractionDelegate {
|
||||
// MARK: MainFeedTableViewSectionHeaderDelegate
|
||||
|
||||
extension MainFeedViewController: MainFeedTableViewSectionHeaderDelegate {
|
||||
|
||||
|
||||
func mainFeedTableViewSectionHeaderDisclosureDidToggle(_ sender: MainFeedTableViewSectionHeader) {
|
||||
toggle(sender)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: MainTableViewCellDelegate
|
||||
|
||||
extension MainFeedViewController: MainFeedTableViewCellDelegate {
|
||||
|
||||
|
||||
func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool) {
|
||||
if expanding {
|
||||
expand(sender)
|
||||
@ -725,7 +733,7 @@ extension MainFeedViewController: MainFeedTableViewCellDelegate {
|
||||
collapse(sender)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
@ -741,28 +749,28 @@ private extension MainFeedViewController {
|
||||
let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||
}
|
||||
|
||||
|
||||
func setFilterButtonToActive() {
|
||||
filterButton?.image = AppAssets.filterActiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
|
||||
}
|
||||
|
||||
|
||||
func setFilterButtonToInactive() {
|
||||
filterButton?.image = AppAssets.filterInactiveImage
|
||||
filterButton?.accLabelText = NSLocalizedString("Filter Read Feeds", comment: "Filter Read Feeds")
|
||||
}
|
||||
|
||||
|
||||
func resetEstimatedRowHeight() {
|
||||
let titleLabel = NonIntrinsicLabel()
|
||||
titleLabel.text = "But I must explain"
|
||||
|
||||
|
||||
let unreadCountView = MainFeedUnreadCountView()
|
||||
unreadCountView.unreadCount = 10
|
||||
|
||||
|
||||
let layout = MainFeedTableViewCellLayout(cellWidth: tableView.bounds.size.width, insets: tableView.safeAreaInsets, label: titleLabel, unreadCountView: unreadCountView, showingEditingControl: false, indent: false, shouldShowDisclosure: false)
|
||||
tableView.estimatedRowHeight = layout.height
|
||||
}
|
||||
|
||||
|
||||
func configure(_ cell: MainFeedTableViewCell, _ indexPath: IndexPath) {
|
||||
guard let node = coordinator.nodeFor(indexPath) else { return }
|
||||
|
||||
@ -772,14 +780,14 @@ private extension MainFeedViewController {
|
||||
} else {
|
||||
cell.indentationLevel = 1
|
||||
}
|
||||
|
||||
|
||||
if let containerID = (node.representedObject as? Container)?.containerID {
|
||||
cell.setDisclosure(isExpanded: coordinator.isExpanded(containerID), animated: false)
|
||||
cell.isDisclosureAvailable = true
|
||||
} else {
|
||||
cell.isDisclosureAvailable = false
|
||||
}
|
||||
|
||||
|
||||
if let feed = node.representedObject as? SidebarItem {
|
||||
cell.name = feed.nameForDisplay
|
||||
cell.unreadCount = feed.unreadCount
|
||||
@ -793,9 +801,9 @@ private extension MainFeedViewController {
|
||||
} else {
|
||||
cell.isSeparatorShown = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func configureIcon(_ cell: MainFeedTableViewCell, _ indexPath: IndexPath) {
|
||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID else {
|
||||
return
|
||||
@ -824,7 +832,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func applyToAvailableCells(_ completion: (MainFeedTableViewCell, IndexPath) -> Void) {
|
||||
for cell in tableView.visibleCells {
|
||||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
@ -839,7 +847,7 @@ private extension MainFeedViewController {
|
||||
tableView.reloadRows(at: indexPaths, with: .none)
|
||||
restoreSelectionIfNecessary(adjustScroll: false)
|
||||
}
|
||||
|
||||
|
||||
private func accountForNode(_ node: Node) -> Account? {
|
||||
if let account = node.representedObject as? Account {
|
||||
return account
|
||||
@ -857,7 +865,7 @@ private extension MainFeedViewController {
|
||||
guard let sectionNode = coordinator.rootNode.childAtIndex(headerView.tag) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if coordinator.isExpanded(sectionNode) {
|
||||
headerView.disclosureExpanded = false
|
||||
coordinator.collapse(sectionNode)
|
||||
@ -885,17 +893,17 @@ private extension MainFeedViewController {
|
||||
return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
|
||||
var menuElements = [UIMenuElement]()
|
||||
|
||||
|
||||
if let inspectorAction = self.getInfoAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [inspectorAction]))
|
||||
}
|
||||
|
||||
|
||||
if let homePageAction = self.homePageAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [homePageAction]))
|
||||
}
|
||||
|
||||
|
||||
var pageActions = [UIAction]()
|
||||
if let copyFeedPageAction = self.copyFeedPageAction(indexPath: indexPath) {
|
||||
pageActions.append(copyFeedPageAction)
|
||||
@ -910,7 +918,7 @@ private extension MainFeedViewController {
|
||||
if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
|
||||
}
|
||||
|
||||
|
||||
if includeDeleteRename {
|
||||
menuElements.append(UIMenu(title: "",
|
||||
options: .displayInline,
|
||||
@ -919,24 +927,24 @@ private extension MainFeedViewController {
|
||||
self.deleteAction(indexPath: indexPath)
|
||||
]))
|
||||
}
|
||||
|
||||
|
||||
return UIMenu(title: "", children: menuElements)
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
|
||||
var menuElements = [UIMenuElement]()
|
||||
|
||||
if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
|
||||
}
|
||||
|
||||
|
||||
menuElements.append(UIMenu(title: "",
|
||||
options: .displayInline,
|
||||
children: [
|
||||
@ -963,14 +971,14 @@ private extension MainFeedViewController {
|
||||
guard coordinator.homePageURLForFeed(indexPath) != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let title = NSLocalizedString("Open Home Page", comment: "Open Home Page")
|
||||
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in
|
||||
self?.coordinator.showBrowserForFeed(indexPath)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func homePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard coordinator.homePageURLForFeed(indexPath) != nil else {
|
||||
return nil
|
||||
@ -983,20 +991,20 @@ private extension MainFeedViewController {
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let url = URL(string: feed.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
|
||||
let action = UIAction(title: title, image: AppAssets.copyImage) { action in
|
||||
UIPasteboard.general.url = url
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let url = URL(string: feed.url) else {
|
||||
@ -1010,21 +1018,21 @@ private extension MainFeedViewController {
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let homePageURL = feed.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
|
||||
UIPasteboard.general.url = url
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
let homePageURL = feed.homePageURL,
|
||||
@ -1039,20 +1047,20 @@ private extension MainFeedViewController {
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||
feed.unreadCount > 0,
|
||||
let articles = try? feed.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 cancel = {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
|
||||
@ -1062,16 +1070,16 @@ private extension MainFeedViewController {
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func deleteAction(indexPath: IndexPath) -> UIAction {
|
||||
let title = NSLocalizedString("Delete", comment: "Delete")
|
||||
|
||||
|
||||
let action = UIAction(title: title, image: AppAssets.trashImage, attributes: .destructive) { [weak self] action in
|
||||
self?.delete(indexPath: indexPath)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func renameAction(indexPath: IndexPath) -> UIAction {
|
||||
let title = NSLocalizedString("Rename", comment: "Rename")
|
||||
let action = UIAction(title: title, image: AppAssets.editImage) { [weak self] action in
|
||||
@ -1079,12 +1087,12 @@ private extension MainFeedViewController {
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func getInfoAction(indexPath: IndexPath) -> UIAction? {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed 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)
|
||||
@ -1127,7 +1135,7 @@ private extension MainFeedViewController {
|
||||
feed.unreadCount > 0 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
|
||||
@ -1168,19 +1176,19 @@ private extension MainFeedViewController {
|
||||
|
||||
let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed")
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
||||
|
||||
|
||||
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
|
||||
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in
|
||||
|
||||
|
||||
guard let name = alertController.textFields?[0].text, !name.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let feed = feed as? Feed {
|
||||
feed.rename(to: name) { result in
|
||||
switch result {
|
||||
@ -1200,23 +1208,23 @@ private extension MainFeedViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
alertController.addAction(renameAction)
|
||||
alertController.preferredAction = renameAction
|
||||
|
||||
|
||||
alertController.addTextField() { textField in
|
||||
textField.text = feed.nameForDisplay
|
||||
textField.placeholder = NSLocalizedString("Name", comment: "Name")
|
||||
}
|
||||
|
||||
|
||||
self.present(alertController, animated: true) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func delete(indexPath: IndexPath) {
|
||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return }
|
||||
|
||||
@ -1231,22 +1239,22 @@ private extension MainFeedViewController {
|
||||
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text")
|
||||
message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String
|
||||
}
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
||||
|
||||
|
||||
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
|
||||
let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] action in
|
||||
self?.performDelete(indexPath: indexPath)
|
||||
}
|
||||
alertController.addAction(deleteAction)
|
||||
alertController.preferredAction = deleteAction
|
||||
|
||||
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func performDelete(indexPath: IndexPath) {
|
||||
guard let undoManager = undoManager,
|
||||
let deleteNode = coordinator.nodeFor(indexPath),
|
||||
@ -1259,15 +1267,15 @@ private extension MainFeedViewController {
|
||||
} else if let feed = deleteNode.representedObject as? Feed {
|
||||
ActivityManager.cleanUp(feed)
|
||||
}
|
||||
|
||||
|
||||
if indexPath == coordinator.currentFeedIndexPath {
|
||||
coordinator.selectFeed(indexPath: nil)
|
||||
}
|
||||
|
||||
|
||||
pushUndoableCommand(deleteCommand)
|
||||
deleteCommand.perform()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension MainFeedViewController: UIGestureRecognizerDelegate {
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
import RSCore
|
||||
import RSWeb
|
||||
import Account
|
||||
import Articles
|
||||
import WebKit
|
||||
@ -56,6 +57,13 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
|
||||
// TODO: fix this temporary hack, which will probably require refactoring image handling.
|
||||
// We want to know when to possibly reconfigure our cells with a new image, and we don’t
|
||||
// always know when an image is available — but watching the .htmlMetadataAvailable Notification
|
||||
// lets us know that it’s time to request an image.
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .htmlMetadataAvailable, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
|
||||
|
Loading…
x
Reference in New Issue
Block a user