2019-04-15 15:03:05 -05:00
|
|
|
|
//
|
2025-01-02 22:18:52 -08:00
|
|
|
|
// MainTimelineViewController.swift
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// NetNewsWire
|
|
|
|
|
//
|
|
|
|
|
// Created by Maurice Parker on 4/8/19.
|
|
|
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
import RSCore
|
2025-01-19 17:59:54 -08:00
|
|
|
|
import RSWeb
|
2019-04-15 15:03:05 -05:00
|
|
|
|
import Account
|
|
|
|
|
import Articles
|
2025-01-15 21:55:09 -08:00
|
|
|
|
import WebKit
|
2019-04-15 15:03:05 -05:00
|
|
|
|
|
2025-01-25 11:01:12 -08:00
|
|
|
|
final class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
2019-04-16 18:08:02 -05:00
|
|
|
|
|
2019-04-29 15:29:00 -05:00
|
|
|
|
private var numberOfTextLines = 0
|
2019-11-16 19:44:01 -06:00
|
|
|
|
private var iconSize = IconSize.medium
|
2025-01-22 22:18:09 -08:00
|
|
|
|
private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showFeedInspector(_:)))
|
|
|
|
|
|
2025-02-02 16:46:49 -08:00
|
|
|
|
private lazy var filterButton = UIBarButtonItem(
|
|
|
|
|
image: AppImage.filterInactive,
|
|
|
|
|
style: .plain,
|
|
|
|
|
target: self,
|
|
|
|
|
action: #selector(toggleFilter(_:))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private lazy var markAllAsReadButton = UIBarButtonItem(
|
|
|
|
|
image: AppImage.markAllAsRead,
|
|
|
|
|
style: .plain,
|
|
|
|
|
target: self,
|
|
|
|
|
action: #selector(markAllAsRead(_:))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private lazy var firstUnreadButton = UIBarButtonItem(
|
|
|
|
|
image: AppImage.nextUnreadArticle,
|
|
|
|
|
style: .plain,
|
|
|
|
|
target: self,
|
|
|
|
|
action: #selector(firstUnread(_:))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private lazy var flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
|
|
|
|
|
|
|
|
|
private lazy var refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
|
|
|
|
private lazy var refreshProgressView: RefreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as! RefreshProgressView
|
2020-08-07 15:20:20 -05:00
|
|
|
|
|
2025-02-02 16:57:15 -08:00
|
|
|
|
private static let cellReuseIdentifier = "timelineCell"
|
|
|
|
|
|
2019-08-30 14:17:05 -05:00
|
|
|
|
private lazy var dataSource = makeDataSource()
|
2019-08-31 11:50:34 -05:00
|
|
|
|
private let searchController = UISearchController(searchResultsController: nil)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-01 12:43:07 -05:00
|
|
|
|
weak var coordinator: SceneCoordinator!
|
2019-04-21 13:57:23 -05:00
|
|
|
|
var undoableCommands = [UndoableCommand]()
|
2020-01-21 11:05:47 -07:00
|
|
|
|
let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0)
|
2019-08-31 11:50:34 -05:00
|
|
|
|
|
2019-09-05 14:37:07 -05:00
|
|
|
|
private let keyboardManager = KeyboardManager(type: .timeline)
|
2019-09-04 16:24:16 -05:00
|
|
|
|
override var keyCommands: [UIKeyCommand]? {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-15 21:55:09 -08:00
|
|
|
|
// If the first responder is the WKWebView we don't want to supply any keyboard
|
2021-06-23 02:44:34 -05:00
|
|
|
|
// 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.
|
2025-01-15 21:55:09 -08:00
|
|
|
|
guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-04 16:24:16 -05:00
|
|
|
|
return keyboardManager.keyCommands
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-16 18:08:02 -05:00
|
|
|
|
override var canBecomeFirstResponder: Bool {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2019-09-04 16:24:16 -05:00
|
|
|
|
|
2025-02-02 16:46:49 -08:00
|
|
|
|
init() {
|
|
|
|
|
super.init(style: .plain)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
override func viewDidLoad() {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
super.viewDidLoad()
|
2019-08-30 14:17:05 -05:00
|
|
|
|
|
2019-04-23 04:35:48 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
2025-01-03 21:30:22 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
2019-08-21 19:37:19 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
2025-01-19 17:59:54 -08:00
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
2019-04-29 15:29:00 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
2019-04-29 14:50:56 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
2019-09-27 19:45:09 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
|
2020-01-06 21:23:39 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-28 21:12:09 -05:00
|
|
|
|
// Setup the Search Controller
|
|
|
|
|
searchController.delegate = self
|
|
|
|
|
searchController.searchResultsUpdater = self
|
|
|
|
|
searchController.obscuresBackgroundDuringPresentation = false
|
|
|
|
|
searchController.searchBar.delegate = self
|
|
|
|
|
searchController.searchBar.placeholder = NSLocalizedString("Search Articles", comment: "Search Articles")
|
|
|
|
|
searchController.searchBar.scopeButtonTitles = [
|
|
|
|
|
NSLocalizedString("Here", comment: "Here"),
|
|
|
|
|
NSLocalizedString("All Articles", comment: "All Articles")
|
|
|
|
|
]
|
2019-11-02 11:49:44 -05:00
|
|
|
|
navigationItem.searchController = searchController
|
2019-10-28 21:12:09 -05:00
|
|
|
|
definesPresentationContext = true
|
|
|
|
|
|
2019-08-31 11:50:34 -05:00
|
|
|
|
// Configure the table
|
2025-02-02 16:57:15 -08:00
|
|
|
|
tableView.register(MainTimelineTableViewCell.self, forCellReuseIdentifier: Self.cellReuseIdentifier)
|
2019-08-31 11:50:34 -05:00
|
|
|
|
tableView.dataSource = dataSource
|
2024-11-11 22:03:32 -08:00
|
|
|
|
tableView.isPrefetchingEnabled = false
|
|
|
|
|
|
2025-01-26 21:06:22 -08:00
|
|
|
|
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
|
|
|
|
iconSize = AppDefaults.timelineIconSize
|
2020-03-18 15:55:33 -05:00
|
|
|
|
resetEstimatedRowHeight()
|
2021-11-18 15:39:05 -06:00
|
|
|
|
|
2025-01-03 13:16:49 -08:00
|
|
|
|
if let titleView = Bundle.main.loadNibNamed("MainTimelineTitleView", owner: self, options: nil)?[0] as? MainTimelineTitleView {
|
2019-11-24 10:47:09 -06:00
|
|
|
|
navigationItem.titleView = titleView
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-03 14:23:37 -07:00
|
|
|
|
refreshControl = UIRefreshControl()
|
|
|
|
|
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-02 16:46:49 -08:00
|
|
|
|
toolbarItems = [
|
|
|
|
|
markAllAsReadButton,
|
|
|
|
|
flexibleSpaceBarButtonItem,
|
|
|
|
|
refreshProgressItemButton,
|
|
|
|
|
flexibleSpaceBarButtonItem,
|
|
|
|
|
firstUnreadButton
|
|
|
|
|
]
|
|
|
|
|
|
2019-12-08 18:14:33 -07:00
|
|
|
|
resetUI(resetScroll: true)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-24 17:34:47 -07:00
|
|
|
|
// Load the table and then scroll to the saved position if available
|
|
|
|
|
applyChanges(animated: false) {
|
|
|
|
|
if let restoreIndexPath = self.coordinator.timelineMiddleIndexPath {
|
|
|
|
|
self.tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
|
|
|
|
|
}
|
2019-11-11 16:59:42 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-20 21:32:19 +08:00
|
|
|
|
// Disable swipe back on iPad Mice
|
2024-11-11 22:03:32 -08:00
|
|
|
|
guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else {
|
|
|
|
|
return
|
2020-05-20 21:32:19 +08:00
|
|
|
|
}
|
2024-11-11 22:03:32 -08:00
|
|
|
|
gesture.allowedScrollTypesMask = []
|
2019-11-11 20:45:14 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-23 18:00:51 -06:00
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
2025-01-03 13:16:49 -08:00
|
|
|
|
self.navigationController?.isToolbarHidden = false
|
|
|
|
|
|
2019-11-23 18:00:51 -06:00
|
|
|
|
// If the nav bar is hidden, fade it in to avoid it showing stuff as it is getting laid out
|
|
|
|
|
if navigationController?.navigationBar.isHidden ?? false {
|
|
|
|
|
navigationController?.navigationBar.alpha = 0
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-03 13:16:49 -08:00
|
|
|
|
super.viewWillAppear(animated)
|
2019-11-23 18:00:51 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-18 19:12:24 -06:00
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
|
|
|
super.viewDidAppear(true)
|
|
|
|
|
coordinator.isTimelineViewControllerPending = false
|
2019-11-23 18:00:51 -06:00
|
|
|
|
|
|
|
|
|
if navigationController?.navigationBar.alpha == 0 {
|
|
|
|
|
UIView.animate(withDuration: 0.5) {
|
|
|
|
|
self.navigationController?.navigationBar.alpha = 1
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-11-18 19:12:24 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-04 21:06:29 -05:00
|
|
|
|
// MARK: Actions
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2021-07-06 18:50:48 -05:00
|
|
|
|
@objc func openInBrowser(_ sender: Any?) {
|
|
|
|
|
coordinator.showBrowserForCurrentArticle()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func openInAppBrowser(_ sender: Any?) {
|
|
|
|
|
coordinator.showInAppBrowser()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-21 18:22:43 -06:00
|
|
|
|
@IBAction func toggleFilter(_ sender: Any) {
|
2020-03-22 10:18:07 -05:00
|
|
|
|
coordinator.toggleReadArticlesFilter()
|
2019-11-21 18:22:43 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
@IBAction func markAllAsRead(_ sender: Any) {
|
2020-01-11 11:30:16 -07:00
|
|
|
|
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-14 17:10:55 +08:00
|
|
|
|
if let source = sender as? UIBarButtonItem {
|
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in
|
|
|
|
|
self?.coordinator.markAllAsReadInTimeline()
|
|
|
|
|
}
|
2020-05-13 12:33:51 +08:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-24 21:57:34 -08:00
|
|
|
|
if sender is UIKeyCommand {
|
2020-05-14 17:10:55 +08:00
|
|
|
|
guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-14 17:10:55 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
|
|
|
|
self?.coordinator.markAllAsReadInTimeline()
|
|
|
|
|
}
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-23 04:35:48 -05:00
|
|
|
|
@IBAction func firstUnread(_ sender: Any) {
|
2019-10-09 21:39:11 -05:00
|
|
|
|
coordinator.selectFirstUnread()
|
2019-04-22 18:00:26 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-03 14:23:37 -07:00
|
|
|
|
@objc func refreshAccounts(_ sender: Any) {
|
|
|
|
|
refreshControl?.endRefreshing()
|
2020-01-10 18:14:21 -07:00
|
|
|
|
|
2020-01-03 14:23:37 -07:00
|
|
|
|
// 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) {
|
2025-02-01 13:28:49 -08:00
|
|
|
|
ManualRefreshNotification.post(errorHandler: ErrorHandler.present(self), sender: self)
|
2020-01-03 14:23:37 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-04 21:06:29 -05:00
|
|
|
|
// MARK: Keyboard shortcuts
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-04 21:06:29 -05:00
|
|
|
|
@objc func selectNextUp(_ sender: Any?) {
|
|
|
|
|
coordinator.selectPrevArticle()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func selectNextDown(_ sender: Any?) {
|
|
|
|
|
coordinator.selectNextArticle()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func navigateToSidebar(_ sender: Any?) {
|
|
|
|
|
coordinator.navigateToFeeds()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-04 21:06:29 -05:00
|
|
|
|
@objc func navigateToDetail(_ sender: Any?) {
|
|
|
|
|
coordinator.navigateToDetail()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-26 14:51:42 +10:00
|
|
|
|
@objc func showFeedInspector(_ sender: Any?) {
|
2019-09-27 19:45:09 -05:00
|
|
|
|
coordinator.showFeedInspector()
|
|
|
|
|
}
|
2020-05-13 17:29:59 +05:30
|
|
|
|
|
2019-08-25 11:38:04 -05:00
|
|
|
|
// MARK: API
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-29 15:53:50 -05:00
|
|
|
|
func restoreSelectionIfNecessary(adjustScroll: Bool) {
|
2019-09-11 09:11:33 -05:00
|
|
|
|
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
|
2019-09-29 15:53:50 -05:00
|
|
|
|
if adjustScroll {
|
2020-01-27 21:57:52 -07:00
|
|
|
|
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: [])
|
2019-09-29 15:53:50 -05:00
|
|
|
|
} else {
|
|
|
|
|
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
|
|
|
|
}
|
2019-09-10 08:06:43 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-08 18:14:33 -07:00
|
|
|
|
func reinitializeArticles(resetScroll: Bool) {
|
|
|
|
|
resetUI(resetScroll: resetScroll)
|
2019-08-25 11:38:04 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-19 11:16:43 -06:00
|
|
|
|
func reloadArticles(animated: Bool) {
|
|
|
|
|
applyChanges(animated: animated)
|
2019-08-25 11:38:04 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-27 21:57:52 -07:00
|
|
|
|
func updateArticleSelection(animations: Animations) {
|
2019-09-11 09:11:33 -05:00
|
|
|
|
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
|
2019-08-25 11:38:04 -05:00
|
|
|
|
if tableView.indexPathForSelectedRow != indexPath {
|
2020-01-27 21:57:52 -07:00
|
|
|
|
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
|
2019-08-25 11:38:04 -05:00
|
|
|
|
}
|
2019-09-04 21:06:29 -05:00
|
|
|
|
} else {
|
2020-01-27 21:57:52 -07:00
|
|
|
|
tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none)
|
2019-08-25 11:38:04 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-25 11:38:04 -05:00
|
|
|
|
updateUI()
|
|
|
|
|
}
|
2020-03-13 16:03:42 -05:00
|
|
|
|
|
|
|
|
|
func updateUI() {
|
2025-02-02 16:46:49 -08:00
|
|
|
|
refreshProgressView.update()
|
2020-03-13 16:03:42 -05:00
|
|
|
|
updateTitleUnreadCount()
|
|
|
|
|
updateToolbar()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-15 17:53:12 -07:00
|
|
|
|
func hideSearch() {
|
|
|
|
|
navigationItem.searchController?.isActive = false
|
|
|
|
|
}
|
2019-08-25 11:38:04 -05:00
|
|
|
|
|
2019-09-01 17:41:46 -05:00
|
|
|
|
func showSearchAll() {
|
|
|
|
|
navigationItem.searchController?.isActive = true
|
|
|
|
|
navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1
|
2019-09-06 10:29:00 -05:00
|
|
|
|
navigationItem.searchController?.searchBar.becomeFirstResponder()
|
2019-09-01 17:41:46 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-04 21:06:29 -05:00
|
|
|
|
func focus() {
|
|
|
|
|
becomeFirstResponder()
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-18 21:45:32 -08:00
|
|
|
|
func setRefreshToolbarItemVisibility(visible: Bool) {
|
2025-02-02 16:46:49 -08:00
|
|
|
|
refreshProgressView.alpha = visible ? 1.0 : 0
|
2025-01-18 21:45:32 -08:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// MARK: - Table view
|
|
|
|
|
|
2019-10-03 15:55:16 -05:00
|
|
|
|
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
2019-09-11 09:11:33 -05:00
|
|
|
|
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
|
2020-02-18 13:49:29 -08:00
|
|
|
|
guard !article.status.read || article.isAvailableToMarkUnread else { return nil }
|
|
|
|
|
|
2019-04-29 06:01:53 -05:00
|
|
|
|
// Set up the read action
|
|
|
|
|
let readTitle = article.status.read ?
|
2021-04-26 08:19:31 +12:00
|
|
|
|
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
|
|
|
|
|
NSLocalizedString("Mark as Read", comment: "Mark as Read")
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
|
|
|
|
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (_, _, completion) in
|
2019-09-11 09:11:33 -05:00
|
|
|
|
self?.coordinator.toggleRead(article)
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-27 22:21:01 -08:00
|
|
|
|
readAction.image = article.status.read ? AppImage.circleClosed : AppImage.circleOpen
|
2025-01-30 12:58:14 -08:00
|
|
|
|
readAction.backgroundColor = AppColor.accent
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-03 15:55:16 -05:00
|
|
|
|
return UISwipeActionsConfiguration(actions: [readAction])
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-03 15:55:16 -05:00
|
|
|
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-03 15:55:16 -05:00
|
|
|
|
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-29 06:01:53 -05:00
|
|
|
|
// Set up the star action
|
|
|
|
|
let starTitle = article.status.starred ?
|
|
|
|
|
NSLocalizedString("Unstar", comment: "Unstar") :
|
|
|
|
|
NSLocalizedString("Star", comment: "Star")
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
|
|
|
|
let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (_, _, completion) in
|
2019-09-11 09:11:33 -05:00
|
|
|
|
self?.coordinator.toggleStar(article)
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-30 12:58:14 -08:00
|
|
|
|
starAction.image = article.status.starred ? AppImage.starOpen : AppImage.starClosed
|
|
|
|
|
starAction.backgroundColor = AppColor.star
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-18 17:34:53 -05:00
|
|
|
|
// Set up the read action
|
|
|
|
|
let moreTitle = NSLocalizedString("More", comment: "More")
|
2019-12-14 17:14:55 -07:00
|
|
|
|
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-18 17:34:53 -05:00
|
|
|
|
if let self = self {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-18 17:34:53 -05:00
|
|
|
|
let alert = UIAlertController(title: nil, 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)
|
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
if let action = self.markAboveAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
alert.addAction(action)
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
alert.addAction(action)
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-14 17:14:55 -07:00
|
|
|
|
if let action = self.discloseFeedAlertAction(article, completion: completion) {
|
2019-08-19 15:45:52 -05:00
|
|
|
|
alert.addAction(action)
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
|
2019-08-19 17:26:09 -05:00
|
|
|
|
alert.addAction(action)
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-14 17:14:55 -07:00
|
|
|
|
if let action = self.openInBrowserAlertAction(article, completion: completion) {
|
2019-08-19 17:38:30 -05:00
|
|
|
|
alert.addAction(action)
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-14 17:14:55 -07:00
|
|
|
|
if let action = self.shareAlertAction(article, indexPath: indexPath, completion: completion) {
|
2019-08-19 18:09:38 -05:00
|
|
|
|
alert.addAction(action)
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-18 17:34:53 -05:00
|
|
|
|
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
|
|
|
|
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-08-18 17:34:53 -05:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
self.present(alert, animated: true)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-18 17:34:53 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-18 17:34:53 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-26 22:19:19 -08:00
|
|
|
|
moreAction.image = AppImage.more
|
2019-08-18 17:34:53 -05:00
|
|
|
|
moreAction.backgroundColor = UIColor.systemGray
|
|
|
|
|
|
2019-10-03 15:55:16 -05:00
|
|
|
|
return UISwipeActionsConfiguration(actions: [starAction, moreAction])
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-16 13:19:06 -05:00
|
|
|
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
|
|
|
|
2019-09-11 09:11:33 -05:00
|
|
|
|
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
|
|
|
|
return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] _ in
|
2020-01-07 16:36:32 -07:00
|
|
|
|
|
|
|
|
|
guard let self = self else { return nil }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-11-13 05:23:04 -06:00
|
|
|
|
var menuElements = [UIMenuElement]()
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-11-13 05:23:04 -06:00
|
|
|
|
var markActions = [UIAction]()
|
2020-02-18 13:49:29 -08:00
|
|
|
|
if let action = self.toggleArticleReadStatusAction(article) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
markActions.append(action)
|
2020-02-18 13:49:29 -08:00
|
|
|
|
}
|
2020-11-13 05:23:04 -06:00
|
|
|
|
markActions.append(self.toggleArticleStarStatusAction(article))
|
2020-05-13 12:33:51 +08:00
|
|
|
|
if let action = self.markAboveAsReadAction(article, indexPath: indexPath) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
markActions.append(action)
|
2019-08-19 18:09:38 -05:00
|
|
|
|
}
|
2020-05-13 12:33:51 +08:00
|
|
|
|
if let action = self.markBelowAsReadAction(article, indexPath: indexPath) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
markActions.append(action)
|
2020-01-07 16:36:32 -07:00
|
|
|
|
}
|
2020-11-13 05:23:04 -06:00
|
|
|
|
menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions))
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-11-13 05:23:04 -06:00
|
|
|
|
var secondaryActions = [UIAction]()
|
2020-01-07 16:36:32 -07:00
|
|
|
|
if let action = self.discloseFeedAction(article) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
secondaryActions.append(action)
|
2020-01-07 16:36:32 -07:00
|
|
|
|
}
|
2020-05-13 12:33:51 +08:00
|
|
|
|
if let action = self.markAllInFeedAsReadAction(article, indexPath: indexPath) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
secondaryActions.append(action)
|
|
|
|
|
}
|
|
|
|
|
if !secondaryActions.isEmpty {
|
|
|
|
|
menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions))
|
2020-01-07 16:36:32 -07:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2021-05-01 16:47:39 -04:00
|
|
|
|
var copyActions = [UIAction]()
|
|
|
|
|
if let action = self.copyArticleURLAction(article) {
|
|
|
|
|
copyActions.append(action)
|
|
|
|
|
}
|
|
|
|
|
if let action = self.copyExternalURLAction(article) {
|
|
|
|
|
copyActions.append(action)
|
|
|
|
|
}
|
|
|
|
|
if !copyActions.isEmpty {
|
|
|
|
|
menuElements.append(UIMenu(title: "", options: .displayInline, children: copyActions))
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-07 16:36:32 -07:00
|
|
|
|
if let action = self.openInBrowserAction(article) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
menuElements.append(UIMenu(title: "", options: .displayInline, children: [action]))
|
2020-01-07 16:36:32 -07:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-07 16:36:32 -07:00
|
|
|
|
if let action = self.shareAction(article, indexPath: indexPath) {
|
2020-11-13 05:23:04 -06:00
|
|
|
|
menuElements.append(UIMenu(title: "", options: .displayInline, children: [action]))
|
2020-01-07 16:36:32 -07:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-11-13 05:23:04 -06:00
|
|
|
|
return UIMenu(title: "", children: menuElements)
|
2020-01-07 16:36:32 -07:00
|
|
|
|
|
|
|
|
|
})
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-16 13:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-23 22:15:29 -06:00
|
|
|
|
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
|
|
|
|
guard let row = configuration.identifier as? Int,
|
|
|
|
|
let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-23 22:15:29 -06:00
|
|
|
|
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
2019-09-04 16:24:16 -05:00
|
|
|
|
becomeFirstResponder()
|
2019-09-11 09:11:33 -05:00
|
|
|
|
let article = dataSource.itemIdentifier(for: indexPath)
|
2020-01-27 21:57:52 -07:00
|
|
|
|
coordinator.selectArticle(article, animations: [.scroll, .select, .navigation])
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-11 16:59:42 -06:00
|
|
|
|
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
2021-12-29 14:44:40 -08:00
|
|
|
|
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
|
2019-11-11 16:59:42 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// MARK: Notifications
|
|
|
|
|
|
2019-04-23 04:35:48 -05:00
|
|
|
|
@objc dynamic func unreadCountDidChange(_ notification: Notification) {
|
|
|
|
|
updateUI()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
@objc func statusesDidChange(_ note: Notification) {
|
2019-12-16 22:45:59 -08:00
|
|
|
|
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String>, !articleIDs.isEmpty else {
|
2019-04-15 15:03:05 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-12-16 22:45:59 -08:00
|
|
|
|
|
2019-09-10 20:32:03 -05:00
|
|
|
|
let visibleArticles = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
|
2019-12-16 22:45:59 -08:00
|
|
|
|
let visibleUpdatedArticles = visibleArticles.filter { articleIDs.contains($0.articleID) }
|
2019-09-06 13:45:45 -05:00
|
|
|
|
|
|
|
|
|
for article in visibleUpdatedArticles {
|
2019-09-11 09:11:33 -05:00
|
|
|
|
if let indexPath = dataSource.indexPath(for: article) {
|
2025-01-02 22:06:19 -08:00
|
|
|
|
if let cell = tableView.cellForRow(at: indexPath) as? MainTimelineTableViewCell {
|
2019-09-06 13:45:45 -05:00
|
|
|
|
configure(cell, article: article)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 11:08:58 -07:00
|
|
|
|
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-02 22:18:52 -08:00
|
|
|
|
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
2019-11-24 10:47:09 -06:00
|
|
|
|
titleView.iconView.iconImage = coordinator.timelineIconImage
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2024-11-01 22:09:22 -07:00
|
|
|
|
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
2019-04-15 15:03:05 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
2024-11-18 15:11:18 -08:00
|
|
|
|
if let indexPaths = tableView.indexPathsForVisibleRows {
|
|
|
|
|
for indexPath in indexPaths {
|
|
|
|
|
guard let article = dataSource.itemIdentifier(for: indexPath) else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-01-02 22:06:19 -08:00
|
|
|
|
if article.feed == feed, let cell = tableView.cellForRow(at: indexPath) as? MainTimelineTableViewCell, let image = iconImageFor(article) {
|
2024-11-18 15:11:18 -08:00
|
|
|
|
cell.setIconImage(image)
|
|
|
|
|
}
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
2019-11-05 18:05:57 -06:00
|
|
|
|
guard coordinator.showIcons, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
2019-04-15 15:03:05 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
2024-11-18 15:11:18 -08:00
|
|
|
|
if let indexPaths = tableView.indexPathsForVisibleRows {
|
|
|
|
|
for indexPath in indexPaths {
|
|
|
|
|
guard let article = dataSource.itemIdentifier(for: indexPath), let authors = article.authors, !authors.isEmpty else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for author in authors {
|
2025-01-02 22:06:19 -08:00
|
|
|
|
if author.avatarURL == avatarURL, let cell = tableView.cellForRow(at: indexPath) as? MainTimelineTableViewCell, let image = iconImageFor(article) {
|
2024-11-18 15:11:18 -08:00
|
|
|
|
cell.setIconImage(image)
|
|
|
|
|
}
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 19:37:19 -05:00
|
|
|
|
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
2025-01-02 22:18:52 -08:00
|
|
|
|
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
2019-11-24 10:47:09 -06:00
|
|
|
|
titleView.iconView.iconImage = coordinator.timelineIconImage
|
|
|
|
|
}
|
2019-11-05 18:05:57 -06:00
|
|
|
|
if coordinator.showIcons {
|
2019-09-06 17:22:12 -05:00
|
|
|
|
queueReloadAvailableCells()
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-29 15:29:00 -05:00
|
|
|
|
@objc func userDefaultsDidChange(_ note: Notification) {
|
2021-03-25 16:28:15 -05:00
|
|
|
|
DispatchQueue.main.async {
|
2025-01-26 21:06:22 -08:00
|
|
|
|
if self.numberOfTextLines != AppDefaults.timelineNumberOfLines || self.iconSize != AppDefaults.timelineIconSize {
|
|
|
|
|
self.numberOfTextLines = AppDefaults.timelineNumberOfLines
|
|
|
|
|
self.iconSize = AppDefaults.timelineIconSize
|
2021-03-25 16:28:15 -05:00
|
|
|
|
self.resetEstimatedRowHeight()
|
|
|
|
|
self.reloadAllVisibleCells()
|
|
|
|
|
}
|
|
|
|
|
self.updateToolbar()
|
2019-04-29 15:29:00 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-29 14:50:56 -05:00
|
|
|
|
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
2019-08-30 14:17:05 -05:00
|
|
|
|
reloadAllVisibleCells()
|
2019-04-29 14:50:56 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-27 19:45:09 -05:00
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
2025-01-02 22:18:52 -08:00
|
|
|
|
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
2019-11-24 10:47:09 -06:00
|
|
|
|
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
|
|
|
|
}
|
2019-09-27 19:45:09 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-01-06 21:23:39 -07:00
|
|
|
|
@objc func willEnterForeground(_ note: Notification) {
|
|
|
|
|
updateUI()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-11 16:59:42 -06:00
|
|
|
|
@objc func scrollPositionDidChange() {
|
|
|
|
|
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// MARK: Reloading
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-06 17:22:12 -05:00
|
|
|
|
func queueReloadAvailableCells() {
|
|
|
|
|
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc private func reloadAllVisibleCells() {
|
2022-02-08 11:57:44 -08:00
|
|
|
|
let visibleArticles = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
|
|
|
|
|
reloadCells(visibleArticles)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-03 13:16:49 -08:00
|
|
|
|
|
2019-08-30 14:17:05 -05:00
|
|
|
|
private func reloadCells(_ articles: [Article]) {
|
|
|
|
|
var snapshot = dataSource.snapshot()
|
|
|
|
|
snapshot.reloadItems(articles)
|
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
|
2019-09-29 15:53:50 -05:00
|
|
|
|
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
2019-08-30 14:17:05 -05:00
|
|
|
|
}
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2021-11-18 17:09:42 -06:00
|
|
|
|
|
2019-04-29 16:29:53 -05:00
|
|
|
|
// MARK: Cell Configuring
|
|
|
|
|
|
2020-03-18 15:55:33 -05:00
|
|
|
|
private func resetEstimatedRowHeight() {
|
2024-11-14 20:16:22 -08:00
|
|
|
|
|
2020-03-18 15:55:33 -05:00
|
|
|
|
let prototypeID = "prototype"
|
2020-04-12 17:12:36 -07:00
|
|
|
|
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
|
2025-01-23 22:52:36 -08:00
|
|
|
|
let prototypeArticle = Article(
|
|
|
|
|
accountID: prototypeID,
|
|
|
|
|
articleID: prototypeID,
|
|
|
|
|
feedID: prototypeID,
|
|
|
|
|
uniqueID: prototypeID,
|
|
|
|
|
title: Constants.prototypeText,
|
|
|
|
|
contentHTML: nil,
|
|
|
|
|
contentText: nil,
|
|
|
|
|
url: nil,
|
|
|
|
|
externalURL: nil,
|
|
|
|
|
summary: nil,
|
|
|
|
|
imageURL: nil,
|
|
|
|
|
datePublished: nil,
|
|
|
|
|
dateModified: nil,
|
|
|
|
|
authors: nil,
|
|
|
|
|
status: status
|
|
|
|
|
)
|
2024-11-14 20:16:22 -08:00
|
|
|
|
|
2025-01-24 15:50:14 -08:00
|
|
|
|
let prototypeCellData = MainTimelineCellData(
|
|
|
|
|
article: prototypeArticle,
|
|
|
|
|
showFeedName: .feed,
|
|
|
|
|
feedName: "Prototype Feed Name",
|
|
|
|
|
byline: nil,
|
|
|
|
|
iconImage: nil,
|
|
|
|
|
showIcon: false,
|
|
|
|
|
numberOfLines: numberOfTextLines,
|
|
|
|
|
iconSize: iconSize
|
|
|
|
|
)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-29 17:19:08 -05:00
|
|
|
|
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
|
2025-01-02 22:06:19 -08:00
|
|
|
|
let layout = MainTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
2020-03-18 15:55:33 -05:00
|
|
|
|
tableView.estimatedRowHeight = layout.height
|
2019-04-29 17:19:08 -05:00
|
|
|
|
} else {
|
2025-01-02 22:06:19 -08:00
|
|
|
|
let layout = MainTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
2020-03-18 15:55:33 -05:00
|
|
|
|
tableView.estimatedRowHeight = layout.height
|
2019-04-29 17:19:08 -05:00
|
|
|
|
}
|
2019-04-29 16:29:53 -05:00
|
|
|
|
}
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-31 15:53:47 -05:00
|
|
|
|
// MARK: Searching
|
2019-08-31 11:50:34 -05:00
|
|
|
|
|
2025-01-03 22:58:25 -08:00
|
|
|
|
extension TimelineViewController: UISearchControllerDelegate {
|
2019-09-02 12:40:14 -05:00
|
|
|
|
|
|
|
|
|
func willPresentSearchController(_ searchController: UISearchController) {
|
|
|
|
|
coordinator.beginSearching()
|
|
|
|
|
searchController.searchBar.showsScopeBar = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func willDismissSearchController(_ searchController: UISearchController) {
|
|
|
|
|
coordinator.endSearching()
|
|
|
|
|
searchController.searchBar.showsScopeBar = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-03 22:58:25 -08:00
|
|
|
|
extension TimelineViewController: UISearchResultsUpdating {
|
2019-08-31 15:53:47 -05:00
|
|
|
|
|
2019-08-31 11:50:34 -05:00
|
|
|
|
func updateSearchResults(for searchController: UISearchController) {
|
2019-08-31 15:53:47 -05:00
|
|
|
|
let searchScope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex)!
|
|
|
|
|
coordinator.searchArticles(searchController.searchBar.text!, searchScope)
|
2019-08-31 11:50:34 -05:00
|
|
|
|
}
|
2019-08-31 15:53:47 -05:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-03 22:58:25 -08:00
|
|
|
|
extension TimelineViewController: UISearchBarDelegate {
|
2019-08-31 15:53:47 -05:00
|
|
|
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
|
|
|
|
let searchScope = SearchScope(rawValue: selectedScope)!
|
|
|
|
|
coordinator.searchArticles(searchBar.text!, searchScope)
|
|
|
|
|
}
|
2019-08-31 11:50:34 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// MARK: Private
|
|
|
|
|
|
2025-01-03 22:58:25 -08:00
|
|
|
|
private extension TimelineViewController {
|
2019-04-22 16:25:16 -05:00
|
|
|
|
|
2019-12-08 18:14:33 -07:00
|
|
|
|
func resetUI(resetScroll: Bool) {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-24 13:37:56 -06:00
|
|
|
|
title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline"
|
|
|
|
|
|
2025-01-02 22:18:52 -08:00
|
|
|
|
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
2020-12-06 16:01:43 -06:00
|
|
|
|
let timelineIconImage = coordinator.timelineIconImage
|
|
|
|
|
titleView.iconView.iconImage = timelineIconImage
|
|
|
|
|
if let preferredColor = timelineIconImage?.preferredColor {
|
|
|
|
|
titleView.iconView.tintColor = UIColor(cgColor: preferredColor)
|
|
|
|
|
} else {
|
|
|
|
|
titleView.iconView.tintColor = nil
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-15 06:19:14 -06:00
|
|
|
|
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
2019-10-25 15:03:13 -05:00
|
|
|
|
updateTitleUnreadCount()
|
2019-09-30 20:01:02 -05:00
|
|
|
|
|
2024-11-01 21:34:08 -07:00
|
|
|
|
if coordinator.timelineFeed is Feed {
|
2020-03-24 16:42:46 -05:00
|
|
|
|
titleView.buttonize()
|
2019-11-24 10:47:09 -06:00
|
|
|
|
titleView.addGestureRecognizer(feedTapGestureRecognizer)
|
|
|
|
|
} else {
|
2020-03-24 16:42:46 -05:00
|
|
|
|
titleView.debuttonize()
|
2019-11-24 10:47:09 -06:00
|
|
|
|
titleView.removeGestureRecognizer(feedTapGestureRecognizer)
|
2019-09-27 19:45:09 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-21 17:59:58 -05:00
|
|
|
|
navigationItem.titleView = titleView
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 11:43:36 -06:00
|
|
|
|
switch coordinator.timelineDefaultReadFilterType {
|
|
|
|
|
case .none, .read:
|
2020-08-07 15:20:20 -05:00
|
|
|
|
navigationItem.rightBarButtonItem = filterButton
|
2019-11-24 03:47:29 -06:00
|
|
|
|
case .alwaysRead:
|
2020-08-07 15:20:20 -05:00
|
|
|
|
navigationItem.rightBarButtonItem = nil
|
2019-11-21 18:22:43 -06:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-27 11:43:36 -06:00
|
|
|
|
if coordinator.isReadArticlesFiltered {
|
2025-02-02 16:46:49 -08:00
|
|
|
|
filterButton.image = AppImage.filterActive
|
|
|
|
|
filterButton.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
|
2019-11-27 11:43:36 -06:00
|
|
|
|
} else {
|
2025-02-02 16:46:49 -08:00
|
|
|
|
filterButton.image = AppImage.filterInactive
|
|
|
|
|
filterButton.accLabelText = NSLocalizedString("Filter Read Articles", comment: "Filter Read Articles")
|
2019-11-27 11:43:36 -06:00
|
|
|
|
}
|
2020-03-22 10:18:07 -05:00
|
|
|
|
|
2019-07-27 14:36:01 -05:00
|
|
|
|
tableView.selectRow(at: nil, animated: false, scrollPosition: .top)
|
2020-10-18 19:59:11 -05:00
|
|
|
|
|
2020-10-17 20:06:53 -05:00
|
|
|
|
if resetScroll {
|
|
|
|
|
let snapshot = dataSource.snapshot()
|
|
|
|
|
if snapshot.sectionIdentifiers.count > 0 && snapshot.itemIdentifiers(inSection: 0).count > 0 {
|
|
|
|
|
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
|
|
|
|
}
|
2019-04-22 16:25:16 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-08 09:19:50 -05:00
|
|
|
|
updateToolbar()
|
2019-04-22 18:00:26 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-08 09:19:50 -05:00
|
|
|
|
func updateToolbar() {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-29 13:35:12 -05:00
|
|
|
|
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
|
|
|
|
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-03-13 16:03:42 -05:00
|
|
|
|
if coordinator.isRootSplitCollapsed {
|
2020-08-07 15:20:20 -05:00
|
|
|
|
if let toolbarItems = toolbarItems, toolbarItems.last != firstUnreadButton {
|
|
|
|
|
var items = toolbarItems
|
|
|
|
|
items.append(firstUnreadButton)
|
|
|
|
|
setToolbarItems(items, animated: false)
|
|
|
|
|
}
|
2020-03-13 16:03:42 -05:00
|
|
|
|
} else {
|
2020-08-07 15:20:20 -05:00
|
|
|
|
if let toolbarItems = toolbarItems, toolbarItems.last == firstUnreadButton {
|
|
|
|
|
let items = Array(toolbarItems[0..<toolbarItems.count - 1])
|
|
|
|
|
setToolbarItems(items, animated: false)
|
|
|
|
|
}
|
2020-03-13 16:03:42 -05:00
|
|
|
|
}
|
2019-04-22 16:25:16 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-25 15:03:13 -05:00
|
|
|
|
func updateTitleUnreadCount() {
|
2025-01-02 22:18:52 -08:00
|
|
|
|
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
2021-10-20 19:03:02 -05:00
|
|
|
|
titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount
|
2019-11-24 10:47:09 -06:00
|
|
|
|
}
|
2019-09-30 20:01:02 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-19 11:16:43 -06:00
|
|
|
|
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
|
2020-03-18 16:06:05 -05:00
|
|
|
|
if coordinator.articles.count == 0 {
|
|
|
|
|
tableView.rowHeight = tableView.estimatedRowHeight
|
|
|
|
|
} else {
|
|
|
|
|
tableView.rowHeight = UITableView.automaticDimension
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-30 14:17:05 -05:00
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
|
|
|
|
|
snapshot.appendSections([0])
|
|
|
|
|
snapshot.appendItems(coordinator.articles, toSection: 0)
|
2020-03-18 16:06:05 -05:00
|
|
|
|
|
2019-11-19 11:16:43 -06:00
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in
|
2019-09-29 15:53:50 -05:00
|
|
|
|
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
2019-08-30 14:17:05 -05:00
|
|
|
|
completion?()
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-30 14:17:05 -05:00
|
|
|
|
func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> {
|
2019-11-19 11:16:43 -06:00
|
|
|
|
let dataSource: UITableViewDiffableDataSource<Int, Article> =
|
2025-01-02 22:18:52 -08:00
|
|
|
|
MainTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in
|
2025-02-02 16:57:15 -08:00
|
|
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellReuseIdentifier, for: indexPath) as! MainTimelineTableViewCell
|
2019-11-19 11:16:43 -06:00
|
|
|
|
self?.configure(cell, article: article)
|
|
|
|
|
return cell
|
|
|
|
|
})
|
2019-11-21 19:54:35 -06:00
|
|
|
|
dataSource.defaultRowAnimation = .middle
|
2019-11-19 11:16:43 -06:00
|
|
|
|
return dataSource
|
2019-08-30 14:17:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-02 22:06:19 -08:00
|
|
|
|
func configure(_ cell: MainTimelineTableViewCell, article: Article) {
|
2025-01-03 13:16:49 -08:00
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
let iconImage = iconImageFor(article)
|
2025-01-03 13:16:49 -08:00
|
|
|
|
|
2019-06-29 13:35:12 -05:00
|
|
|
|
let showFeedNames = coordinator.showFeedNames
|
2019-11-05 18:05:57 -06:00
|
|
|
|
let showIcon = coordinator.showIcons && iconImage != nil
|
2025-01-24 21:57:34 -08:00
|
|
|
|
cell.cellData = MainTimelineCellData(
|
|
|
|
|
article: article,
|
|
|
|
|
showFeedName: showFeedNames,
|
|
|
|
|
feedName: article.feed?.nameForDisplay,
|
|
|
|
|
byline: article.byline(),
|
|
|
|
|
iconImage: iconImage,
|
|
|
|
|
showIcon: showIcon,
|
|
|
|
|
numberOfLines: numberOfTextLines,
|
|
|
|
|
iconSize: iconSize
|
|
|
|
|
)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
func iconImageFor(_ article: Article) -> IconImage? {
|
2025-02-02 16:47:35 -08:00
|
|
|
|
coordinator.showIcons ? article.iconImage() : nil
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-02-18 13:49:29 -08:00
|
|
|
|
func toggleArticleReadStatusAction(_ article: Article) -> UIAction? {
|
|
|
|
|
guard !article.status.read || article.isAvailableToMarkUnread else { return nil }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-16 13:19:06 -05:00
|
|
|
|
let title = article.status.read ?
|
|
|
|
|
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
|
|
|
|
|
NSLocalizedString("Mark as Read", comment: "Mark as Read")
|
2025-01-27 22:21:01 -08:00
|
|
|
|
let image = article.status.read ? AppImage.circleClosed : AppImage.circleOpen
|
2019-08-16 13:19:06 -05:00
|
|
|
|
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAction(title: title, image: image) { [weak self] _ in
|
2019-09-11 09:11:33 -05:00
|
|
|
|
self?.coordinator.toggleRead(article)
|
2019-08-16 13:19:06 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-16 13:19:06 -05:00
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-11 09:11:33 -05:00
|
|
|
|
func toggleArticleStarStatusAction(_ article: Article) -> UIAction {
|
2019-08-16 13:19:06 -05:00
|
|
|
|
|
|
|
|
|
let title = article.status.starred ?
|
|
|
|
|
NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") :
|
|
|
|
|
NSLocalizedString("Mark as Starred", comment: "Mark as Starred")
|
2025-01-30 12:58:14 -08:00
|
|
|
|
let image = article.status.starred ? AppImage.starOpen : AppImage.starClosed
|
2019-08-16 13:19:06 -05:00
|
|
|
|
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAction(title: title, image: image) { [weak self] _ in
|
2019-09-11 09:11:33 -05:00
|
|
|
|
self?.coordinator.toggleStar(article)
|
2019-08-16 13:19:06 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-16 13:19:06 -05:00
|
|
|
|
return action
|
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
func markAboveAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
|
|
|
|
guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
|
2025-01-26 22:19:19 -08:00
|
|
|
|
let image = AppImage.markAboveAsRead
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAction(title: title, image: image) { [weak self] _ in
|
2020-05-13 12:33:51 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
2020-01-11 11:30:16 -07:00
|
|
|
|
self?.coordinator.markAboveAsRead(article)
|
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
|
|
|
|
guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
2025-01-26 22:19:19 -08:00
|
|
|
|
let image = AppImage.markBelowAsRead
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAction(title: title, image: image) { [weak self] _ in
|
2020-05-13 12:33:51 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
2020-01-11 11:30:16 -07:00
|
|
|
|
self?.coordinator.markBelowAsRead(article)
|
|
|
|
|
}
|
2019-08-18 17:34:53 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
|
|
|
|
guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
|
2020-01-11 11:30:16 -07:00
|
|
|
|
let cancel = {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
completion(true)
|
|
|
|
|
}
|
2020-01-11 11:30:16 -07:00
|
|
|
|
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
|
2020-05-13 12:33:51 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
|
2020-01-11 11:30:16 -07:00
|
|
|
|
self?.coordinator.markAboveAsRead(article)
|
|
|
|
|
completion(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-03 08:16:55 +01:00
|
|
|
|
return action
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
func markBelowAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
|
|
|
|
guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
2020-01-03 08:16:55 +01:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
2020-01-11 11:30:16 -07:00
|
|
|
|
let cancel = {
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-08-18 17:34:53 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
|
|
|
|
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
|
2020-05-13 12:33:51 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
|
2020-01-11 11:30:16 -07:00
|
|
|
|
self?.coordinator.markBelowAsRead(article)
|
|
|
|
|
completion(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-18 17:34:53 -05:00
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-03 13:16:49 -08:00
|
|
|
|
|
2019-09-11 09:11:33 -05:00
|
|
|
|
func discloseFeedAction(_ article: Article) -> UIAction? {
|
2024-11-02 11:08:58 -07:00
|
|
|
|
guard let feed = article.feed,
|
|
|
|
|
!coordinator.timelineFeedIsEqualTo(feed) else { return nil }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-13 15:22:22 -06:00
|
|
|
|
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
|
2025-01-26 22:19:19 -08:00
|
|
|
|
let action = UIAction(title: title, image: AppImage.openInSidebar) { [weak self] _ in
|
2024-11-02 11:08:58 -07:00
|
|
|
|
self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation])
|
2019-08-19 15:45:52 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-14 17:14:55 -07:00
|
|
|
|
func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
2024-11-02 11:08:58 -07:00
|
|
|
|
guard let feed = article.feed,
|
|
|
|
|
!coordinator.timelineFeedIsEqualTo(feed) else { return nil }
|
2019-09-11 09:11:33 -05:00
|
|
|
|
|
2019-11-13 15:22:22 -06:00
|
|
|
|
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
|
2024-11-02 11:08:58 -07:00
|
|
|
|
self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation])
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-08-19 15:45:52 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
2024-11-02 11:08:58 -07:00
|
|
|
|
guard let feed = article.feed else { return nil }
|
|
|
|
|
guard let fetchedArticles = try? feed.fetchArticles() else {
|
2019-12-16 22:45:59 -08:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-09-11 09:11:33 -05:00
|
|
|
|
|
2019-12-16 22:45:59 -08:00
|
|
|
|
let articles = Array(fetchedArticles)
|
2020-05-13 12:33:51 +08:00
|
|
|
|
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
2019-08-19 17:26:09 -05:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-19 17:26:09 -05:00
|
|
|
|
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
2024-11-02 11:08:58 -07:00
|
|
|
|
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-01-26 22:07:13 -08:00
|
|
|
|
let action = UIAction(title: title, image: AppImage.markAllAsRead) { [weak self] _ in
|
2020-05-13 12:33:51 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
2020-01-11 11:30:16 -07:00
|
|
|
|
self?.coordinator.markAllAsRead(articles)
|
|
|
|
|
}
|
2019-08-19 17:26:09 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 12:33:51 +08:00
|
|
|
|
func markAllInFeedAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
2024-11-02 11:08:58 -07:00
|
|
|
|
guard let feed = article.feed else { return nil }
|
|
|
|
|
guard let fetchedArticles = try? feed.fetchArticles() else {
|
2019-12-16 22:45:59 -08:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-16 22:45:59 -08:00
|
|
|
|
let articles = Array(fetchedArticles)
|
2020-05-13 12:33:51 +08:00
|
|
|
|
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
2019-08-19 17:26:09 -05:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-19 17:38:30 -05:00
|
|
|
|
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed")
|
2024-11-02 11:08:58 -07:00
|
|
|
|
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
2020-01-11 11:30:16 -07:00
|
|
|
|
let cancel = {
|
|
|
|
|
completion(true)
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
|
|
|
|
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
|
2020-05-13 12:33:51 +08:00
|
|
|
|
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
|
2020-01-11 11:30:16 -07:00
|
|
|
|
self?.coordinator.markAllAsRead(articles)
|
|
|
|
|
completion(true)
|
|
|
|
|
}
|
2019-08-19 17:26:09 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2021-05-01 16:47:39 -04:00
|
|
|
|
func copyArticleURLAction(_ article: Article) -> UIAction? {
|
2021-09-12 21:34:47 -05:00
|
|
|
|
guard let url = article.preferredURL else { return nil }
|
2021-05-01 16:47:39 -04:00
|
|
|
|
let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL")
|
2025-01-26 22:19:19 -08:00
|
|
|
|
let action = UIAction(title: title, image: AppImage.copy) { _ in
|
2021-05-01 16:47:39 -04:00
|
|
|
|
UIPasteboard.general.url = url
|
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2021-05-01 16:47:39 -04:00
|
|
|
|
func copyExternalURLAction(_ article: Article) -> UIAction? {
|
2021-09-30 16:46:11 +13:00
|
|
|
|
guard let externalLink = article.externalLink, externalLink != article.preferredLink, let url = URL(string: externalLink) else { return nil }
|
2021-05-01 16:47:39 -04:00
|
|
|
|
let title = NSLocalizedString("Copy External URL", comment: "Copy External URL")
|
2025-01-26 22:19:19 -08:00
|
|
|
|
let action = UIAction(title: title, image: AppImage.copy) { _ in
|
2021-05-01 16:47:39 -04:00
|
|
|
|
UIPasteboard.general.url = url
|
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-11 09:11:33 -05:00
|
|
|
|
func openInBrowserAction(_ article: Article) -> UIAction? {
|
2025-01-24 21:57:34 -08:00
|
|
|
|
guard article.preferredURL != nil else { return nil }
|
2019-08-19 17:38:30 -05:00
|
|
|
|
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
2025-01-26 22:19:19 -08:00
|
|
|
|
let action = UIAction(title: title, image: AppImage.safari) { [weak self] _ in
|
2019-09-11 09:11:33 -05:00
|
|
|
|
self?.coordinator.showBrowserForArticle(article)
|
2019-08-19 17:38:30 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-14 17:14:55 -07:00
|
|
|
|
func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
2025-01-24 21:57:34 -08:00
|
|
|
|
guard article.preferredURL != nil else { return nil }
|
2021-08-26 10:27:23 +08:00
|
|
|
|
|
|
|
|
|
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
|
2019-09-11 09:11:33 -05:00
|
|
|
|
self?.coordinator.showBrowserForArticle(article)
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-08-19 17:38:30 -05:00
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-19 18:09:38 -05:00
|
|
|
|
func shareDialogForTableCell(indexPath: IndexPath, url: URL, title: String?) {
|
2020-01-11 09:12:41 +01:00
|
|
|
|
let activityViewController = UIActivityViewController(url: url, title: title, applicationActivities: nil)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-19 18:09:38 -05:00
|
|
|
|
guard let cell = tableView.cellForRow(at: indexPath) else { return }
|
|
|
|
|
let popoverController = activityViewController.popoverPresentationController
|
|
|
|
|
popoverController?.sourceView = cell
|
|
|
|
|
popoverController?.sourceRect = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-08-19 18:09:38 -05:00
|
|
|
|
present(activityViewController, animated: true)
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-11 09:11:33 -05:00
|
|
|
|
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
2021-04-26 09:28:19 +12:00
|
|
|
|
guard let url = article.preferredURL else { return nil }
|
2019-08-19 18:09:38 -05:00
|
|
|
|
let title = NSLocalizedString("Share", comment: "Share")
|
2025-01-27 22:21:01 -08:00
|
|
|
|
let action = UIAction(title: title, image: AppImage.share) { [weak self] _ in
|
2019-08-19 18:09:38 -05:00
|
|
|
|
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
|
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-14 17:14:55 -07:00
|
|
|
|
func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
2021-04-26 09:28:19 +12:00
|
|
|
|
guard let url = article.preferredURL else { return nil }
|
2019-08-19 18:09:38 -05:00
|
|
|
|
let title = NSLocalizedString("Share", comment: "Share")
|
2025-01-22 22:18:09 -08:00
|
|
|
|
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
|
2019-12-14 17:14:55 -07:00
|
|
|
|
completion(true)
|
2019-08-19 18:09:38 -05:00
|
|
|
|
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
|
|
|
|
|
}
|
|
|
|
|
return action
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|