diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 966d4b6b1..dd300884a 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -63,6 +63,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var crashReportWindowController: CrashReportWindowController? // For testing only private let log = Log() private let appNewsURLString = "https://nnw.ranchero.com/feed.json" + private let appMovementMonitor = RSAppMovementMonitor() override init() { NSWindow.allowsAutomaticWindowTabbing = false diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css index 6cbf20640..dd821b6b3 100644 --- a/Mac/MainWindow/Detail/styleSheet.css +++ b/Mac/MainWindow/Detail/styleSheet.css @@ -124,7 +124,7 @@ pre { } img, figure, video, iframe { max-width: 100%; - height: auto; + height: auto !important; margin: 0 auto; } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 5bd756139..5ca775c33 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 5110AB7822B7BD6200A94F76 /* AddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5110AB7722B7BD6200A94F76 /* AddView.swift */; }; 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; }; 511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; @@ -34,6 +33,7 @@ 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */; }; 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; }; 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; }; + 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */; }; 51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51543684228F6753005E1CDF /* DetailAccountViewController.swift */; }; 515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */; }; 5154368A2291FED9005E1CDF /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */; }; @@ -686,7 +686,6 @@ 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = ""; }; 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeedbinAccountView.swift; sourceTree = ""; }; 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = ""; }; - 5110AB7722B7BD6200A94F76 /* AddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddView.swift; sourceTree = ""; }; 51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; @@ -707,6 +706,7 @@ 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedbinWindowController.swift; sourceTree = ""; }; 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedbin.xib; sourceTree = ""; }; 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; + 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddControllerType.swift; sourceTree = ""; }; 51543684228F6753005E1CDF /* DetailAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountViewController.swift; sourceTree = ""; }; 515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalAccountViewController.swift; sourceTree = ""; }; 515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; @@ -1028,18 +1028,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 5110AB6E22B7BD3C00A94F76 /* UIKit */ = { - isa = PBXGroup; - children = ( - 51C452822265093600C03939 /* Add.storyboard */, - 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */, - 51C452842265093600C03939 /* AddFeedViewController.swift */, - 51C452812265093600C03939 /* AddFeedFolderPickerData.swift */, - 51C4528B2265095F00C03939 /* AddFolderViewController.swift */, - ); - path = UIKit; - sourceTree = ""; - }; 511D43CE231FA51100FB1562 /* Resources */ = { isa = PBXGroup; children = ( @@ -1217,8 +1205,12 @@ 51C452802265093600C03939 /* Add */ = { isa = PBXGroup; children = ( - 5110AB6E22B7BD3C00A94F76 /* UIKit */, - 5110AB7722B7BD6200A94F76 /* AddView.swift */, + 51C452822265093600C03939 /* Add.storyboard */, + 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */, + 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */, + 51C452842265093600C03939 /* AddFeedViewController.swift */, + 51C452812265093600C03939 /* AddFeedFolderPickerData.swift */, + 51C4528B2265095F00C03939 /* AddFolderViewController.swift */, ); path = Add; sourceTree = ""; @@ -2434,7 +2426,6 @@ files = ( 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */, 512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */, - 5110AB7822B7BD6200A94F76 /* AddView.swift in Sources */, 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, 51EF0F79227716380050506E /* ColorHash.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, @@ -2477,6 +2468,7 @@ 51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */, 51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */, 51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */, + 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */, 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */, 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */, 51EF0F7C2277919E0050506E /* TimelineNumberOfLinesViewController.swift in Sources */, diff --git a/Shared/Resources/GlobalKeyboardShortcuts.plist b/Shared/Resources/GlobalKeyboardShortcuts.plist index c5117654e..ee971ae4c 100644 --- a/Shared/Resources/GlobalKeyboardShortcuts.plist +++ b/Shared/Resources/GlobalKeyboardShortcuts.plist @@ -19,8 +19,6 @@ goToPreviousUnread: - title - Go to Previous Unread key [uparrow] shiftModifier @@ -29,16 +27,12 @@ goToPreviousUnread: - title - Next Unread key + action nextUnread: - title - Next Unread key + shiftModifier diff --git a/Shared/Resources/SidebarKeyboardShortcuts.plist b/Shared/Resources/SidebarKeyboardShortcuts.plist index f34f1d8d7..9ad1d2c5f 100644 --- a/Shared/Resources/SidebarKeyboardShortcuts.plist +++ b/Shared/Resources/SidebarKeyboardShortcuts.plist @@ -10,7 +10,7 @@ title - Collapse Selected Rows + Collapse Selected Row key , action @@ -18,7 +18,7 @@ title - Expand Selected Rows + Expand Selected Row key . action diff --git a/iOS/Add/UIKit/Add.storyboard b/iOS/Add/Add.storyboard similarity index 100% rename from iOS/Add/UIKit/Add.storyboard rename to iOS/Add/Add.storyboard diff --git a/iOS/Add/UIKit/AddContainerViewController.swift b/iOS/Add/AddContainerViewController.swift similarity index 93% rename from iOS/Add/UIKit/AddContainerViewController.swift rename to iOS/Add/AddContainerViewController.swift index 16d92719c..dc574c02f 100644 --- a/iOS/Add/UIKit/AddContainerViewController.swift +++ b/iOS/Add/AddContainerViewController.swift @@ -33,6 +33,7 @@ class AddContainerViewController: UIViewController { private var currentViewController: AddContainerViewControllerChild? + var initialControllerType: AddControllerType? var initialFeed: String? var initialFeedName: String? @@ -40,20 +41,26 @@ class AddContainerViewController: UIViewController { super.viewDidLoad() activityIndicatorView.isHidden = true - - switchToFeed() + + typeSelectorSegmentedControl.selectedSegmentIndex = initialControllerType?.rawValue ?? 0 + switch initialControllerType { + case .feed: + switchToFeed() + case .folder: + switchToFolder() + default: + assertionFailure() + } } @IBAction func typeSelectorChanged(_ sender: UISegmentedControl) { - switch sender.selectedSegmentIndex { case 0: switchToFeed() default: switchToFolder() } - } @IBAction func cancel(_ sender: Any) { diff --git a/iOS/Add/AddControllerType.swift b/iOS/Add/AddControllerType.swift new file mode 100644 index 000000000..cf68e97af --- /dev/null +++ b/iOS/Add/AddControllerType.swift @@ -0,0 +1,14 @@ +// +// AddControllerType.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 9/5/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation + +enum AddControllerType: Int { + case feed = 0 + case folder = 1 +} diff --git a/iOS/Add/UIKit/AddFeedFolderPickerData.swift b/iOS/Add/AddFeedFolderPickerData.swift similarity index 100% rename from iOS/Add/UIKit/AddFeedFolderPickerData.swift rename to iOS/Add/AddFeedFolderPickerData.swift diff --git a/iOS/Add/UIKit/AddFeedViewController.swift b/iOS/Add/AddFeedViewController.swift similarity index 99% rename from iOS/Add/UIKit/AddFeedViewController.swift rename to iOS/Add/AddFeedViewController.swift index 2d6295c62..a6736dbba 100644 --- a/iOS/Add/UIKit/AddFeedViewController.swift +++ b/iOS/Add/AddFeedViewController.swift @@ -44,6 +44,7 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh urlTextField.autocapitalizationType = .none urlTextField.text = initialFeed urlTextField.delegate = self + urlTextField.becomeFirstResponder() if initialFeed != nil { delegate?.readyToAdd(state: true) diff --git a/iOS/Add/UIKit/AddFolderViewController.swift b/iOS/Add/AddFolderViewController.swift similarity index 98% rename from iOS/Add/UIKit/AddFolderViewController.swift rename to iOS/Add/AddFolderViewController.swift index 3e7849937..77844d460 100644 --- a/iOS/Add/UIKit/AddFolderViewController.swift +++ b/iOS/Add/AddFolderViewController.swift @@ -31,6 +31,8 @@ class AddFolderViewController: UITableViewController, AddContainerViewController accounts = AccountManager.shared.sortedActiveAccounts nameTextField.delegate = self + nameTextField.becomeFirstResponder() + accountLabel.text = (accounts[0] as DisplayNameProvider).nameForDisplay if shouldDisplayPicker { diff --git a/iOS/Add/AddView.swift b/iOS/Add/AddView.swift deleted file mode 100644 index 0c57b1a97..000000000 --- a/iOS/Add/AddView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// AddView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 6/17/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct AddView : View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/) - } -} - -#if DEBUG -struct AddView_Previews : PreviewProvider { - static var previews: some View { - AddView() - } -} -#endif diff --git a/iOS/Detail/DetailViewController.swift b/iOS/Detail/DetailViewController.swift index 83ee0762c..f60ec3bad 100644 --- a/iOS/Detail/DetailViewController.swift +++ b/iOS/Detail/DetailViewController.swift @@ -26,15 +26,11 @@ class DetailViewController: UIViewController { weak var coordinator: SceneCoordinator! - lazy var keyboardManager = KeyboardManager(type: .detail, coordinator: coordinator) + private let keyboardManager = KeyboardManager(type: .detail) override var keyCommands: [UIKeyCommand]? { return keyboardManager.keyCommands } - override var canBecomeFirstResponder: Bool { - return true - } - deinit { webView.removeFromSuperview() DetailViewControllerWebViewProvider.shared.enqueueWebView(webView) @@ -155,7 +151,7 @@ class DetailViewController: UIViewController { } @IBAction func toggleStar(_ sender: Any) { - coordinator.toggleStarForCurrentArticle() + coordinator.toggleStarredForCurrentArticle() } @IBAction func openBrowser(_ sender: Any) { @@ -188,7 +184,28 @@ class DetailViewController: UIViewController { webView.becomeFirstResponder() } + func finalScrollPosition() -> CGFloat { + return webView.scrollView.contentSize.height - webView.scrollView.bounds.size.height + webView.scrollView.contentInset.bottom + } + + func canScrollDown() -> Bool { + return webView.scrollView.contentOffset.y < finalScrollPosition() + } + + func scrollPageDown() { + let scrollToY: CGFloat = { + let fullScroll = webView.scrollView.contentOffset.y + webView.scrollView.bounds.size.height + let final = finalScrollPosition() + return fullScroll < final ? fullScroll : final + }() + + let convertedPoint = self.view.convert(CGPoint(x: 0, y: 0), to: webView.scrollView) + let scrollToPoint = CGPoint(x: convertedPoint.x, y: scrollToY) + webView.scrollView.setContentOffset(scrollToPoint, animated: true) + } + } +//print("\(candidateY) : \(webView.scrollView.contentSize.height)") class ArticleActivityItemSource: NSObject, UIActivityItemSource { diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index 7bef03e86..0af53e80e 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -17,36 +17,27 @@ enum KeyboardType: String { class KeyboardManager { - private let coordinator: SceneCoordinator private(set) var keyCommands: [UIKeyCommand]? - init(type: KeyboardType, coordinator: SceneCoordinator) { - self.coordinator = coordinator - load(type: type) + init(type: KeyboardType) { + let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")! + let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]] + keyCommands = globalEntries.compactMap { createKeyCommand(keyEntry: $0) } + keyCommands!.append(contentsOf: globalAuxilaryKeyCommands()) + + let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")! + let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]] + keyCommands!.append(contentsOf: specificEntries.compactMap { createKeyCommand(keyEntry: $0) } ) + + if type == .sidebar { + keyCommands!.append(contentsOf: sidebarAuxilaryKeyCommands()) + } } } private extension KeyboardManager { - func load(type: KeyboardType) { - let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")! - let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]] - var globalCommands = globalEntries.compactMap { createKeyCommand(keyEntry: $0) } - - let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")! - let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]] - let specificCommands = specificEntries.compactMap { createKeyCommand(keyEntry: $0) } - - globalCommands.append(contentsOf: specificCommands) - - if type == .sidebar { - globalCommands.append(contentsOf: sidebarAuxilaryKeyCommands()) - } - - keyCommands = globalCommands - } - func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? { guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil } let modifiers = createKeyModifierFlags(keyEntry: keyEntry) @@ -69,7 +60,7 @@ private extension KeyboardManager { switch(key) { case "[space]": - return " " + return "\u{0020}" case "[uparrow]": return UIKeyCommand.inputUpArrow case "[downarrow]": @@ -116,6 +107,48 @@ private extension KeyboardManager { return flags } + func globalAuxilaryKeyCommands() -> [UIKeyCommand] { + var keys = [UIKeyCommand]() + + let addNewFeedTitle = NSLocalizedString("New Feed", comment: "New Feed") + keys.append(createKeyCommand(title: addNewFeedTitle, action: "addNewFeed:", input: "n", modifiers: [.command])) + + let addNewFolderTitle = NSLocalizedString("New Folder", comment: "New Folder") + keys.append(createKeyCommand(title: addNewFolderTitle, action: "addNewFolder:", input: "n", modifiers: [.command, .shift])) + + let refreshTitle = NSLocalizedString("Refresh", comment: "Refresh") + keys.append(createKeyCommand(title: refreshTitle, action: "refresh:", input: "r", modifiers: [.command])) + + let nextUnreadTitle = NSLocalizedString("Next Unread", comment: "Next Unread") + keys.append(createKeyCommand(title: nextUnreadTitle, action: "nextUnread:", input: "/", modifiers: [.command])) + + let goToTodayTitle = NSLocalizedString("Go To Today", comment: "Go To Today") + keys.append(createKeyCommand(title: goToTodayTitle, action: "goToToday:", input: "1", modifiers: [.command])) + + let goToAllUnreadTitle = NSLocalizedString("Go To All Unread", comment: "Go To All Unread") + keys.append(createKeyCommand(title: goToAllUnreadTitle, action: "goToAllUnread:", input: "2", modifiers: [.command])) + + let goToStarredTitle = NSLocalizedString("Go To Starred", comment: "Go To Starred") + keys.append(createKeyCommand(title: goToStarredTitle, action: "goToStarred:", input: "3", modifiers: [.command])) + + let toggleReadTitle = NSLocalizedString("Toggle Read Status", comment: "Toggle Read Status") + keys.append(createKeyCommand(title: toggleReadTitle, action: "toggleRead:", input: "U", modifiers: [.command, .shift])) + + let markAllAsReadTitle = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") + keys.append(createKeyCommand(title: markAllAsReadTitle, action: "markAllAsRead:", input: "k", modifiers: [.command])) + + let markOlderAsReadTitle = NSLocalizedString("Mark Older as Read", comment: "Mark Older as Read") + keys.append(createKeyCommand(title: markOlderAsReadTitle, action: "markOlderArticlesAsRead:", input: "k", modifiers: [.command, .shift])) + + let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status") + keys.append(createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift])) + + let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser") + keys.append(createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command])) + + return keys + } + func sidebarAuxilaryKeyCommands() -> [UIKeyCommand] { var keys = [UIKeyCommand]() diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 301401607..04fb25d19 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -22,7 +22,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { var undoableCommands = [UndoableCommand]() weak var coordinator: SceneCoordinator! - lazy var keyboardManager = KeyboardManager(type: .sidebar, coordinator: coordinator) + private let keyboardManager = KeyboardManager(type: .sidebar) override var keyCommands: [UIKeyCommand]? { return keyboardManager.keyCommands } @@ -56,6 +56,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { updateUI() applyChanges(animate: false) + becomeFirstResponder() } @@ -259,7 +260,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { becomeFirstResponder() - coordinator.selectFeed(indexPath) + coordinator.selectFeed(indexPath, automated: false) } override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { @@ -347,7 +348,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } @IBAction func add(_ sender: UIBarButtonItem) { - coordinator.showAdd() + coordinator.showAdd(.feed) } @objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) { @@ -361,11 +362,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { if coordinator.isExpanded(sectionNode) { headerView.disclosureExpanded = false - coordinator.collapse(section: sectionIndex) + coordinator.collapseSection(sectionIndex) self.applyChanges(animate: true) } else { headerView.disclosureExpanded = true - coordinator.expand(section: sectionIndex) + coordinator.expandSection(sectionIndex) self.applyChanges(animate: true) } @@ -398,6 +399,36 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { coordinator.showBrowserForCurrentFeed() } + @objc override func delete(_ sender: Any?) { + if let indexPath = coordinator.currentFeedIndexPath { + delete(indexPath: indexPath) + } + } + + @objc func expandSelectedRows(_ sender: Any?) { + if let indexPath = coordinator.currentFeedIndexPath { + coordinator.expandFolder(indexPath) + self.applyChanges(animate: true) + } + } + + @objc func collapseSelectedRows(_ sender: Any?) { + if let indexPath = coordinator.currentFeedIndexPath { + coordinator.collapseFolder(indexPath) + self.applyChanges(animate: true) + } + } + + @objc func expandAll(_ sender: Any?) { + coordinator.expandAllSectionsAndFolders() + self.applyChanges(animate: true) + } + + @objc func collapseAllExceptForGroupItems(_ sender: Any?) { + coordinator.collapseAllFolders() + self.applyChanges(animate: true) + } + // MARK: API func updateFeedSelection() { @@ -421,24 +452,42 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } + func ensureSectionIsExpanded(_ sectionIndex: Int, completion: (() -> Void)? = nil) { + guard let sectionNode = coordinator.rootNode.childAtIndex(sectionIndex) else { + return + } + + if !coordinator.isExpanded(sectionNode) { + coordinator.expandSection(sectionIndex) + self.applyChanges(animate: true) { + completion?() + } + } else { + completion?() + } + } + func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) { guard let node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else { - return + completion?() + return } if let indexPath = coordinator.indexPathFor(node) { tableView.scrollToRow(at: indexPath, at: .middle, animated: true) coordinator.selectFeed(indexPath) + completion?() return } // It wasn't already visable, so expand its folder and try again guard let parent = node.parent, let indexPath = coordinator.indexPathFor(parent) else { + completion?() return } - coordinator.expand(indexPath) + coordinator.expandFolder(indexPath) reloadNode(parent) self.applyChanges(animate: true) { [weak self] in @@ -481,12 +530,10 @@ private extension MasterFeedViewController { } func reloadNode(_ node: Node) { - let savedNode = selectedNode() - var snapshot = dataSource.snapshot() snapshot.reloadItems([node]) dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in - self?.selectRow(node: savedNode) + self?.restoreSelectionIfNecessary() } } @@ -504,21 +551,6 @@ private extension MasterFeedViewController { completion?() } } - - func selectedNode() -> Node? { - if let selectedIndexPath = tableView.indexPathForSelectedRow { - return coordinator.nodeFor(selectedIndexPath) - } else { - return nil - } - } - - func selectRow(node: Node?) { - if let nodeToSelect = node, let selectingIndexPath = coordinator.indexPathFor(nodeToSelect) { - tableView.selectRow(at: selectingIndexPath, animated: false, scrollPosition: .none) - } - - } func makeDataSource() -> UITableViewDiffableDataSource { return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in @@ -613,7 +645,7 @@ private extension MasterFeedViewController { guard let indexPath = tableView.indexPath(for: cell) else { return } - coordinator.expand(indexPath) + coordinator.expandFolder(indexPath) self.applyChanges(animate: true) } @@ -621,7 +653,7 @@ private extension MasterFeedViewController { guard let indexPath = tableView.indexPath(for: cell) else { return } - coordinator.collapse(indexPath) + coordinator.collapseFolder(indexPath) self.applyChanges(animate: true) } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 0b6ab4efd..bfa04de25 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -24,7 +24,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner weak var coordinator: SceneCoordinator! var undoableCommands = [UndoableCommand]() - lazy var keyboardManager = KeyboardManager(type: .timeline, coordinator: coordinator) + private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { return keyboardManager.keyCommands } @@ -156,6 +156,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } func updateArticleSelection(animate: Bool) { + guard !coordinator.articles.isEmpty else { return } + if let indexPath = coordinator.currentArticleIndexPath { if tableView.indexPathForSelectedRow != indexPath { tableView.selectRow(at: indexPath, animated: animate, scrollPosition: .middle) @@ -163,12 +165,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } else { tableView.selectRow(at: nil, animated: animate, scrollPosition: .none) } + updateUI() } func showSearchAll() { navigationItem.searchController?.isActive = true navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1 + navigationItem.searchController?.searchBar.becomeFirstResponder() } func focus() { diff --git a/iOS/Resources/styleSheet.css b/iOS/Resources/styleSheet.css index 2a8be9316..9c1a7fcf1 100644 --- a/iOS/Resources/styleSheet.css +++ b/iOS/Resources/styleSheet.css @@ -122,7 +122,7 @@ pre { } img, figure, video, iframe { max-width: 100%; - height: auto; + height: auto !important; margin: 0 auto; } diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index fe63a7d28..c02d23594 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import Account class RootSplitViewController: UISplitViewController { @@ -14,8 +15,82 @@ class RootSplitViewController: UISplitViewController { // MARK: Keyboard Shortcuts + @objc func scrollOrGoToNextUnread(_ sender: Any?) { + coordinator.scrollOrGoToNextUnread() + } + + @objc func goToPreviousUnread(_ sender: Any?) { + coordinator.selectPrevUnread() + } + + @objc func nextUnread(_ sender: Any?) { + coordinator.selectNextUnread() + } + + @objc func markRead(_ sender: Any?) { + coordinator.markAsReadForCurrentArticle() + } + + @objc func markUnreadAndGoToNextUnread(_ sender: Any?) { + coordinator.markAsUnreadForCurrentArticle() + coordinator.selectNextUnread() + } + + @objc func markAllAsReadAndGoToNextUnread(_ sender: Any?) { + coordinator.markAllAsReadInTimeline() + coordinator.selectNextUnread() + } + + @objc func markOlderArticlesAsRead(_ sender: Any?) { + coordinator.markAsReadOlderArticlesInTimeline() + } + + @objc func markUnread(_ sender: Any?) { + coordinator.markAsUnreadForCurrentArticle() + } + + @objc func goToPreviousSubscription(_ sender: Any?) { + coordinator.selectPrevFeed() + } + + @objc func goToNextSubscription(_ sender: Any?) { + coordinator.selectNextFeed() + } + @objc func openInBrowser(_ sender: Any?) { coordinator.showBrowserForCurrentArticle() } + @objc func addNewFeed(_ sender: Any?) { + coordinator.showAdd(.feed) + } + + @objc func addNewFolder(_ sender: Any?) { + coordinator.showAdd(.folder) + } + + @objc func refresh(_ sender: Any?) { + AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) + } + + @objc func goToToday(_ sender: Any?) { + coordinator.selectTodayFeed() + } + + @objc func goToAllUnread(_ sender: Any?) { + coordinator.selectAllUnreadFeed() + } + + @objc func goToStarred(_ sender: Any?) { + coordinator.selectStarredFeed() + } + + @objc func toggleRead(_ sender: Any?) { + coordinator.toggleReadForCurrentArticle() + } + + @objc func toggleStarred(_ sender: Any?) { + coordinator.toggleStarredForCurrentArticle() + } + } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index c9a458735..3fc78544a 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -108,7 +108,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { var timelineFetcher: ArticleFetcher? { didSet { - selectArticle(nil) if timelineFetcher is Feed { showFeedNames = false } else { @@ -300,6 +299,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func handle(_ activity: NSUserActivity) { + selectFeed(nil) + guard let activityType = ActivityType(rawValue: activity.activityType) else { return } switch activityType { case .selectToday: @@ -326,7 +327,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func showSearch() { selectFeed(nil) - masterTimelineViewController?.showSearchAll() + + masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) + masterTimelineViewController!.coordinator = self + navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: false) + + DispatchQueue.main.asyncAfter(deadline: .now()) { + self.masterTimelineViewController!.showSearchAll() + } } // MARK: Notifications @@ -436,10 +444,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return 0 } - func expand(section: Int) { - guard let expandNode = treeController.rootNode.childAtIndex(section) else { + func expandSection(_ section: Int) { + guard let expandNode = treeController.rootNode.childAtIndex(section), !expandedNodes.contains(expandNode) else { return } + expandedNodes.append(expandNode) animatingChanges = true @@ -463,8 +472,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { animatingChanges = false } - func expand(_ indexPath: IndexPath) { + func expandAllSectionsAndFolders() { + for (sectionIndex, sectionNode) in treeController.rootNode.childNodes.enumerated() { + + expandSection(sectionIndex) + + for topLevelNode in sectionNode.childNodes { + if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) { + expandFolder(indexPath) + } + } + + } + } + + func expandFolder(_ indexPath: IndexPath) { let expandNode = shadowTable[indexPath.section][indexPath.row] + guard !expandedNodes.contains(expandNode) else { return } expandedNodes.append(expandNode) animatingChanges = true @@ -479,13 +503,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { animatingChanges = false } - func collapse(section: Int) { - animatingChanges = true - - guard let collapseNode = treeController.rootNode.childAtIndex(section) else { + func collapseSection(_ section: Int) { + guard let collapseNode = treeController.rootNode.childAtIndex(section), expandedNodes.contains(collapseNode) else { return } + animatingChanges = true + if let removeNode = expandedNodes.firstIndex(of: collapseNode) { expandedNodes.remove(at: removeNode) } @@ -495,10 +519,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { animatingChanges = false } - func collapse(_ indexPath: IndexPath) { + func collapseAllFolders() { + for sectionNode in treeController.rootNode.childNodes { + for topLevelNode in sectionNode.childNodes { + if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) { + collapseFolder(indexPath) + } + } + } + } + + func collapseFolder(_ indexPath: IndexPath) { animatingChanges = true let collapseNode = shadowTable[indexPath.section][indexPath.row] + guard expandedNodes.contains(collapseNode) else { return } if let removeNode = expandedNodes.firstIndex(of: collapseNode) { expandedNodes.remove(at: removeNode) } @@ -540,24 +575,28 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return indexes } - func selectFeed(_ indexPath: IndexPath?) { - if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 { - masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) - masterTimelineViewController!.coordinator = self - navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: true) - } - + func selectFeed(_ indexPath: IndexPath?, automated: Bool = true) { + selectArticle(nil) currentFeedIndexPath = indexPath if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher { timelineFetcher = fetcher updateSelectingActivity(with: node) + + if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 { + masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) + masterTimelineViewController!.coordinator = self + navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: !automated) + } } else { timelineFetcher = nil + + if rootSplitViewController.isCollapsed && navControllerForTimeline().viewControllers.last is MasterTimelineViewController { + navControllerForTimeline().popViewController(animated: !automated) + } } masterFeedViewController.updateFeedSelection() - selectArticle(nil) } func selectPrevFeed() { @@ -571,6 +610,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { selectFeed(indexPath) } } + + func selectTodayFeed() { + masterFeedViewController?.ensureSectionIsExpanded(0) { + self.selectFeed(IndexPath(row: 0, section: 0)) + } + } + + func selectAllUnreadFeed() { + masterFeedViewController?.ensureSectionIsExpanded(0) { + self.selectFeed(IndexPath(row: 1, section: 0)) + } + } + + func selectStarredFeed() { + masterFeedViewController?.ensureSectionIsExpanded(0) { + self.selectFeed(IndexPath(row: 2, section: 0)) + } + } func selectArticle(_ indexPath: IndexPath?, automated: Bool = true) { currentArticleIndexPath = indexPath @@ -581,18 +638,22 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } if indexPath == nil { - if !rootSplitViewController.isCollapsed { + if rootSplitViewController.isCollapsed { + if masterNavigationController.children.last is DetailViewController { + masterNavigationController.popViewController(animated: !automated) + } + } else { let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self) - installDetailController(systemMessageViewController) + installDetailController(systemMessageViewController, automated: automated) } - masterTimelineViewController?.updateArticleSelection(animate: true) + masterTimelineViewController?.updateArticleSelection(animate: !automated) return } if detailViewController == nil { let detailViewController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self) detailViewController.coordinator = self - installDetailController(detailViewController) + installDetailController(detailViewController, automated: automated) } // Automatically hide the overlay @@ -604,7 +665,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } if automated { - masterTimelineViewController?.updateArticleSelection(animate: true) + masterTimelineViewController?.updateArticleSelection(animate: false) } detailViewController?.updateArticleSelection() @@ -672,6 +733,22 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } + func selectPrevUnread() { + + // This should never happen, but I don't want to risk throwing us + // into an infinate loop searching for an unread that isn't there. + if appDelegate.unreadCount < 1 { + return + } + + if selectPrevUnreadArticleInTimeline() { + return + } + + selectPrevUnreadFeedFetcher() + selectPrevUnreadArticleInTimeline() + } + func selectNextUnread() { // This should never happen, but I don't want to risk throwing us @@ -692,6 +769,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } + func scrollOrGoToNextUnread() { + if detailViewController?.canScrollDown() ?? false { + detailViewController?.scrollPageDown() + } else { + selectNextUnread() + } + } + func markAllAsRead(_ articles: [Article]) { guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { return @@ -713,6 +798,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { masterNavigationController.popViewController(animated: true) } + func markAsReadOlderArticlesInTimeline() { + if let indexPath = currentArticleIndexPath { + markAsReadOlderArticlesInTimeline(indexPath) + } + } func markAsReadOlderArticlesInTimeline(_ indexPath: IndexPath) { let article = articles[indexPath.row] let articlesToMark = articles.filter { $0.logicalDatePublished < article.logicalDatePublished } @@ -722,6 +812,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { markAllAsRead(articlesToMark) } + func markAsReadForCurrentArticle() { + if let article = currentArticle { + markArticles(Set([article]), statusKey: .read, flag: true) + } + } + + func markAsUnreadForCurrentArticle() { + if let article = currentArticle { + markArticles(Set([article]), statusKey: .read, flag: false) + } + } + func toggleReadForCurrentArticle() { if let article = currentArticle { markArticles(Set([article]), statusKey: .read, flag: !article.status.read) @@ -737,7 +839,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { runCommand(markReadCommand) } - func toggleStarForCurrentArticle() { + func toggleStarredForCurrentArticle() { if let article = currentArticle { markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred) } @@ -753,7 +855,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) { - masterNavigationController.popViewController(animated: true) masterFeedViewController.discloseFeed(feed) { completion?() } @@ -770,8 +871,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { // self.present(settings, animated: true) } - func showAdd() { - let addViewController = UIStoryboard.add.instantiateInitialViewController()! + func showAdd(_ type: AddControllerType) { + selectFeed(nil) + + let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController + let containerController = addViewController.topViewController as! AddContainerViewController + containerController.initialControllerType = type addViewController.modalPresentationStyle = .formSheet addViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay masterFeedViewController.present(addViewController, animated: true) @@ -819,10 +924,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func navigateToTimeline() { - masterTimelineViewController?.focus() if currentArticleIndexPath == nil { selectArticle(IndexPath(row: 0, section: 0)) } + masterTimelineViewController?.focus() } func navigateToDetail() { @@ -984,11 +1089,113 @@ private extension SceneCoordinator { self.showAvatars = false } - // MARK: Select Next Unread + // MARK: Select Prev Unread + @discardableResult + func selectPrevUnreadArticleInTimeline() -> Bool { + let startingRow: Int = { + if let indexPath = currentArticleIndexPath { + return indexPath.row - 1 + } else { + return articles.count - 1 + } + }() + + return selectPrevArticleInTimeline(startingRow: startingRow) + } + + func selectPrevArticleInTimeline(startingRow: Int) -> Bool { + + guard startingRow >= 0 else { + return false + } + + for i in (0...startingRow).reversed() { + let article = articles[i] + if !article.status.read { + selectArticle(IndexPath(row: i, section: 0)) + return true + } + } + + return false + + } + + func selectPrevUnreadFeedFetcher() { + + let indexPath: IndexPath = { + if currentFeedIndexPath == nil { + return IndexPath(row: 0, section: 0) + } else { + return currentFeedIndexPath! + } + }() + + // Increment or wrap around the IndexPath + let nextIndexPath: IndexPath = { + if indexPath.row - 1 < 0 { + if indexPath.section - 1 < 0 { + return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1) + } else { + return IndexPath(row: shadowTable[indexPath.section - 1].count - 1, section: indexPath.section - 1) + } + } else { + return IndexPath(row: indexPath.row - 1, section: indexPath.section) + } + }() + + if selectPrevUnreadFeedFetcher(startingWith: nextIndexPath) { + return + } + let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1) + selectPrevUnreadFeedFetcher(startingWith: maxIndexPath) + + } + + @discardableResult + func selectPrevUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool { + + for i in (0...indexPath.section).reversed() { + + let startingRow: Int = { + if indexPath.section == i { + return indexPath.row + } else { + return shadowTable[i].count - 1 + } + }() + + for j in (0...startingRow).reversed() { + + let prevIndexPath = IndexPath(row: j, section: i) + guard let node = nodeFor(prevIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else { + assertionFailure() + return true + } + + if expandedNodes.contains(node) { + continue + } + + if unreadCountProvider.unreadCount > 0 { + selectFeed(prevIndexPath) + return true + } + + } + + } + + return false + + } + + // MARK: Select Next Unread + @discardableResult func selectFirstUnreadArticleInTimeline() -> Bool { - return selectArticleInTimeline(startingRow: 0) + return selectNextArticleInTimeline(startingRow: 0) } @discardableResult @@ -1001,10 +1208,10 @@ private extension SceneCoordinator { } }() - return selectArticleInTimeline(startingRow: startingRow) + return selectNextArticleInTimeline(startingRow: startingRow) } - func selectArticleInTimeline(startingRow: Int) -> Bool { + func selectNextArticleInTimeline(startingRow: Int) -> Bool { guard startingRow < articles.count else { return false @@ -1024,10 +1231,13 @@ private extension SceneCoordinator { func selectNextUnreadFeedFetcher() { - guard let indexPath = currentFeedIndexPath else { - assertionFailure() - return - } + let indexPath: IndexPath = { + if currentFeedIndexPath == nil { + return IndexPath(row: -1, section: 0) + } else { + return currentFeedIndexPath! + } + }() // Increment or wrap around the IndexPath let nextIndexPath: IndexPath = { @@ -1054,7 +1264,15 @@ private extension SceneCoordinator { for i in indexPath.section.. Void) { @@ -66,7 +67,7 @@ private extension SceneDelegate { case "com.ranchero.NetNewsWire.ShowSearch": coordinator.showSearch() case "com.ranchero.NetNewsWire.ShowAdd": - coordinator.showAdd() + coordinator.showAdd(.feed) default: break } diff --git a/submodules/RSCore b/submodules/RSCore index b8656655f..50cf102ac 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit b8656655f68f207bf9d14e9fda2c928c1bcbe0cf +Subproject commit 50cf102acd0592ec3bff2446f19386b6593e1ff8