From 9f5fc9093a6fb42e67ab6b7f5b97afd83a3bb9e3 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 19 Jan 2025 17:59:54 -0800 Subject: [PATCH] Reload cells when HTMLMetadata is available. --- iOS/MainFeed/MainFeedViewController.swift | 304 +++++++++--------- iOS/MainTimeline/TimelineViewController.swift | 8 + 2 files changed, 164 insertions(+), 148 deletions(-) diff --git a/iOS/MainFeed/MainFeedViewController.swift b/iOS/MainFeed/MainFeedViewController.swift index 2d9100649..b8f481ac2 100644 --- a/iOS/MainFeed/MainFeedViewController.swift +++ b/iOS/MainFeed/MainFeedViewController.swift @@ -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 { diff --git a/iOS/MainTimeline/TimelineViewController.swift b/iOS/MainTimeline/TimelineViewController.swift index 6aca46cbf..226e10d5a 100644 --- a/iOS/MainTimeline/TimelineViewController.swift +++ b/iOS/MainTimeline/TimelineViewController.swift @@ -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)