Merge branch 'extension-point'

This commit is contained in:
Maurice Parker
2020-04-24 13:34:13 -05:00
163 changed files with 6165 additions and 480 deletions

View File

@@ -29,23 +29,32 @@ class AddFeedController: AddFeedWindowControllerDelegate {
private var titleFromFeed: String?
init(hostWindow: NSWindow) {
self.hostWindow = hostWindow
}
func showAddFeedSheet(_ urlString: String?, _ name: String?, _ account: Account?, _ folder: Folder?) {
func showAddFeedSheet(_ type: AddFeedWindowControllerType, _ urlString: String? = nil, _ name: String? = nil, _ account: Account? = nil, _ folder: Folder? = nil) {
let folderTreeControllerDelegate = FolderTreeControllerDelegate()
let folderTreeController = TreeController(delegate: folderTreeControllerDelegate)
addFeedWindowController = AddFeedWindowController(urlString: urlString ?? urlStringFromPasteboard, name: name, account: account, folder: folder, folderTreeController: folderTreeController, delegate: self)
switch type {
case .webFeed:
addFeedWindowController = AddWebFeedWindowController(urlString: urlString ?? urlStringFromPasteboard,
name: name,
account: account,
folder: folder,
folderTreeController: folderTreeController,
delegate: self)
case .twitterFeed:
addFeedWindowController = AddTwitterFeedWindowController(folderTreeController: folderTreeController,
delegate: self)
}
addFeedWindowController!.runSheetOnWindow(hostWindow)
}
// MARK: AddFeedWindowControllerDelegate
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL url: URL, userEnteredTitle title: String?, container: Container) {
closeAddFeedSheet(NSApplication.ModalResponse.OK)
guard let accountAndFolderSpecifier = accountAndFolderFromContainer(container) else {
@@ -81,11 +90,9 @@ class AddFeedController: AddFeedWindowControllerDelegate {
}
beginShowingProgress()
}
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController) {
closeAddFeedSheet(NSApplication.ModalResponse.cancel)
}
@@ -106,7 +113,6 @@ private extension AddFeedController {
}
func accountAndFolderFromContainer(_ container: Container) -> AccountAndFolderSpecifier? {
if let account = container as? Account {
return AccountAndFolderSpecifier(account: account, folder: nil)
}
@@ -117,7 +123,6 @@ private extension AddFeedController {
}
func closeAddFeedSheet(_ returnCode: NSApplication.ModalResponse) {
if let sheetWindow = addFeedWindowController?.window {
hostWindow.endSheet(sheetWindow, returnCode: returnCode)
}
@@ -126,17 +131,14 @@ private extension AddFeedController {
// MARK: Errors
func showAlreadySubscribedError(_ urlString: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Already subscribed", comment: "Feed finder")
alert.informativeText = NSLocalizedString("Cant add this feed because youve already subscribed to it.", comment: "Feed finder")
alert.beginSheetModal(for: hostWindow)
}
func showInitialDownloadError(_ error: Error) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Download Error", comment: "Feed finder")
@@ -144,31 +146,27 @@ private extension AddFeedController {
let formatString = NSLocalizedString("Cant add this feed because of a download error: “%@”", comment: "Feed finder")
let errorText = NSString.localizedStringWithFormat(formatString as NSString, error.localizedDescription)
alert.informativeText = errorText as String
alert.beginSheetModal(for: hostWindow)
}
func showNoFeedsErrorMessage() {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Feed not found", comment: "Feed finder")
alert.informativeText = NSLocalizedString("Cant add a feed because no feed was found.", comment: "Feed finder")
alert.beginSheetModal(for: hostWindow)
}
// MARK: Progress
func beginShowingProgress() {
runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment:"Feed finder"))
}
func endShowingProgress() {
stopIndeterminateProgress()
hostWindow.makeKeyAndOrderFront(self)
}
}

View File

@@ -0,0 +1,30 @@
//
// AddFeedWIndowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/21/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
enum AddFeedWindowControllerType {
case webFeed
case twitterFeed
}
protocol AddFeedWindowControllerDelegate: class {
// userEnteredURL will have already been validated and normalized.
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, container: Container)
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
}
protocol AddFeedWindowController {
var window: NSWindow? { get }
func runSheetOnWindow(_ hostWindow: NSWindow)
}

View File

@@ -0,0 +1,198 @@
//
// AddTwitterFeedWindowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/21/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import AppKit
import RSCore
import RSTree
import Articles
import Account
class AddTwitterFeedWindowController : NSWindowController, AddFeedWindowController {
@IBOutlet weak var typePopupButton: NSPopUpButton!
@IBOutlet weak var typeDescriptionLabel: NSTextField!
@IBOutlet weak var accountLabel: NSTextField!
@IBOutlet weak var accountPopupButton: NSPopUpButton!
@IBOutlet weak var screenSearchTextField: NSTextField!
@IBOutlet var nameTextField: NSTextField!
@IBOutlet var addButton: NSButton!
@IBOutlet var folderPopupButton: NSPopUpButton!
private var urlString: String?
private var initialName: String?
private weak var initialAccount: Account?
private var initialFolder: Folder?
private weak var delegate: AddFeedWindowControllerDelegate?
private var folderTreeController: TreeController!
private var userEnteredScreenSearch: String? {
var s = screenSearchTextField.stringValue
s = s.collapsingWhitespace
if s.isEmpty {
return nil
}
return s
}
private var userEnteredTitle: String? {
var s = nameTextField.stringValue
s = s.collapsingWhitespace
if s.isEmpty {
return nil
}
return s
}
var hostWindow: NSWindow!
convenience init(folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
self.init(windowNibName: NSNib.Name("AddTwitterFeedSheet"))
self.folderTreeController = folderTreeController
self.delegate = delegate
}
func runSheetOnWindow(_ hostWindow: NSWindow) {
hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in
}
}
override func windowDidLoad() {
let accountMenu = NSMenu()
for feedProvider in ExtensionPointManager.shared.activeFeedProviders {
if let twitterFeedProvider = feedProvider as? TwitterFeedProvider {
let accountMenuItem = NSMenuItem()
accountMenuItem.title = "@\(twitterFeedProvider.screenName)"
accountMenu.addItem(accountMenuItem)
}
}
accountPopupButton.menu = accountMenu
folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode)
if let container = AddWebFeedDefaultContainer.defaultContainer {
if let folder = container as? Folder, let account = folder.account {
FolderTreeMenu.select(account: account, folder: folder, in: folderPopupButton)
} else {
if let account = container as? Account {
FolderTreeMenu.select(account: account, folder: nil, in: folderPopupButton)
}
}
}
updateUI()
}
// MARK: Actions
@IBAction func selectedType(_ sender: Any) {
screenSearchTextField.stringValue = ""
updateUI()
}
@IBAction func cancel(_ sender: Any?) {
cancelSheet()
}
@IBAction func addFeed(_ sender: Any?) {
guard let type = TwitterFeedType(rawValue: typePopupButton.selectedItem?.tag ?? 0),
let atUsername = accountPopupButton.selectedItem?.title else { return }
let username = String(atUsername[atUsername.index(atUsername.startIndex, offsetBy: 1)..<atUsername.endIndex])
var screenSearch = userEnteredScreenSearch
if let screenName = screenSearch, type == .screenName && screenName.starts(with: "@") {
screenSearch = String(screenName[screenName.index(screenName.startIndex, offsetBy: 1)..<screenName.endIndex])
}
guard let url = TwitterFeedProvider.buildURL(type, username: username, screenName: screenSearch, searchField: screenSearch) else { return }
let container = selectedContainer()!
AddWebFeedDefaultContainer.saveDefaultContainer(container)
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container)
}
}
extension AddTwitterFeedWindowController: NSTextFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
updateUI()
}
}
private extension AddTwitterFeedWindowController {
private func updateUI() {
switch typePopupButton.selectedItem?.tag ?? 0 {
case 0:
accountLabel.isHidden = false
accountPopupButton.isHidden = false
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets from everyone you follow", comment: "Home Timeline")
screenSearchTextField.isHidden = true
addButton.isEnabled = true
case 1:
accountLabel.isHidden = false
accountPopupButton.isHidden = false
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets mentioning you", comment: "Mentions")
screenSearchTextField.isHidden = true
addButton.isEnabled = true
case 2:
accountLabel.isHidden = true
accountPopupButton.isHidden = true
var screenSearch = userEnteredScreenSearch
if screenSearch != nil {
if let screenName = screenSearch, screenName.starts(with: "@") {
screenSearch = String(screenName[screenName.index(screenName.startIndex, offsetBy: 1)..<screenName.endIndex])
}
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets from @\(screenSearch!)", comment: "Home Timeline")
} else {
typeDescriptionLabel.stringValue = ""
}
screenSearchTextField.placeholderString = NSLocalizedString("@name", comment: "@name")
screenSearchTextField.isHidden = false
addButton.isEnabled = !screenSearchTextField.stringValue.isEmpty
default:
accountLabel.isHidden = true
accountPopupButton.isHidden = true
if !screenSearchTextField.stringValue.isEmpty {
typeDescriptionLabel.stringValue = NSLocalizedString("Tweets that contain \(screenSearchTextField.stringValue)", comment: "Home Timeline")
} else {
typeDescriptionLabel.stringValue = ""
}
screenSearchTextField.placeholderString = nil
screenSearchTextField.isHidden = false
addButton.isEnabled = !screenSearchTextField.stringValue.isEmpty
}
}
func cancelSheet() {
delegate?.addFeedWindowControllerUserDidCancel(self)
}
func selectedContainer() -> Container? {
return folderPopupButton.selectedItem?.representedObject as? Container
}
}

View File

@@ -12,15 +12,7 @@ import RSTree
import Articles
import Account
protocol AddFeedWindowControllerDelegate: class {
// userEnteredURL will have already been validated and normalized.
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, container: Container)
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
}
class AddFeedWindowController : NSWindowController {
class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
@IBOutlet var urlTextField: NSTextField!
@IBOutlet var nameTextField: NSTextField!
@@ -46,7 +38,7 @@ class AddFeedWindowController : NSWindowController {
var hostWindow: NSWindow!
convenience init(urlString: String?, name: String?, account: Account?, folder: Folder?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
self.init(windowNibName: NSNib.Name("AddFeedSheet"))
self.init(windowNibName: NSNib.Name("AddWebFeedSheet"))
self.urlString = urlString
self.initialName = name
self.initialAccount = account
@@ -127,7 +119,7 @@ class AddFeedWindowController : NSWindowController {
}
}
private extension AddFeedWindowController {
private extension AddWebFeedWindowController {
private func updateUI() {
addButton.isEnabled = urlTextField.stringValue.mayBeURL

View File

@@ -1,13 +1,15 @@
// Add the mouse listeners for the above functions
function linkHover() {
window.onmouseover = function(event) {
if (event.target.matches('a')) {
window.webkit.messageHandlers.mouseDidEnter.postMessage(event.target.href);
var closestAnchor = event.target.closest('a')
if (closestAnchor) {
window.webkit.messageHandlers.mouseDidEnter.postMessage(closestAnchor.href);
}
}
window.onmouseout = function(event) {
if (event.target.matches('a')) {
window.webkit.messageHandlers.mouseDidExit.postMessage(event.target.href);
var closestAnchor = event.target.closest('a')
if (closestAnchor) {
window.webkit.messageHandlers.mouseDidExit.postMessage(closestAnchor.href);
}
}
}

View File

@@ -18,25 +18,20 @@ import RSCore
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
return proposedServices + SharingServicePickerDelegate.customSharingServices(for: items)
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? {
return sharingServiceDelegate
}
private static let sendToCommands: [SendToCommand] = {
return [SendToMicroBlogCommand(), SendToMarsEditCommand()]
}()
static func customSharingServices(for items: [Any]) -> [NSSharingService] {
let customServices = sendToCommands.compactMap { (sendToCommand) -> NSSharingService? in
let customServices = ExtensionPointManager.shared.activeSendToCommands.compactMap { (sendToCommand) -> NSSharingService? in
guard let object = items.first else {
return nil
}
guard sendToCommand.canSendObject(object, selectedText: nil) else {
return nil
}

View File

@@ -570,11 +570,11 @@ private extension SidebarOutlineDataSource {
// Show the add-feed sheet.
if let account = parentNode.representedObject as? Account {
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: nil)
appDelegate.addWebFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: nil)
} else {
let account = parentNode.parent?.representedObject as? Account
let folder = parentNode.representedObject as? Folder
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
appDelegate.addWebFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
}
return true

View File

@@ -129,7 +129,7 @@ private extension SidebarViewController {
let menu = NSMenu(title: "")
menu.addItem(withTitle: NSLocalizedString("New Feed", comment: "Command"), action: #selector(AppDelegate.showAddFeedWindow(_:)), keyEquivalent: "")
menu.addItem(withTitle: NSLocalizedString("New Feed", comment: "Command"), action: #selector(AppDelegate.showAddWebFeedWindow(_:)), keyEquivalent: "")
menu.addItem(withTitle: NSLocalizedString("New Folder", comment: "Command"), action: #selector(AppDelegate.showAddFolderWindow(_:)), keyEquivalent: "")
return menu

View File

@@ -15,14 +15,15 @@ struct TimelineCellData {
let text: String
let dateString: String
let feedName: String
let showFeedName: Bool
let byline: String
let showFeedName: TimelineShowFeedName
let iconImage: IconImage? // feed icon, user avatar, or favicon
let showIcon: Bool // Make space even when icon is nil
let featuredImage: NSImage? // image from within the article
let read: Bool
let starred: Bool
init(article: Article, showFeedName: Bool, feedName: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
self.title = ArticleStringFormatter.truncatedTitle(article)
self.text = ArticleStringFormatter.truncatedSummary(article)
@@ -31,10 +32,15 @@ struct TimelineCellData {
if let feedName = feedName {
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
}
else {
} else {
self.feedName = ""
}
if let byline = byline {
self.byline = byline
} else {
self.byline = ""
}
self.showFeedName = showFeedName
@@ -51,7 +57,8 @@ struct TimelineCellData {
self.text = ""
self.dateString = ""
self.feedName = ""
self.showFeedName = false
self.byline = ""
self.showFeedName = .none
self.showIcon = false
self.iconImage = nil
self.featuredImage = nil

View File

@@ -171,7 +171,7 @@ private extension TimelineCellLayout {
}
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
if !cellData.showFeedName {
if cellData.showFeedName == .none {
return NSZeroRect
}

View File

@@ -248,11 +248,14 @@ private extension TimelineTableCellView {
}
func updateFeedNameView() {
if cellData.showFeedName {
switch cellData.showFeedName {
case .byline:
showView(feedNameView)
updateTextFieldText(feedNameView, cellData.byline)
case .feed:
showView(feedNameView)
updateTextFieldText(feedNameView, cellData.feedName)
}
else {
case .none:
hideView(feedNameView)
}
}

View File

@@ -18,6 +18,12 @@ protocol TimelineDelegate: class {
func timelineInvalidatedRestorationState(_: TimelineViewController)
}
enum TimelineShowFeedName {
case none
case byline
case feed
}
final class TimelineViewController: NSViewController, UndoableCommandRunner, UnreadCountProvider {
@IBOutlet var tableView: TimelineTableView!
@@ -41,23 +47,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
unreadCount = 0
if let representedObjects = representedObjects {
if representedObjects.count == 1 && representedObjects.first is WebFeed {
showFeedNames = false
}
else {
showFeedNames = true
}
}
else {
showFeedNames = false
}
selectionDidChange(nil)
if showsSearchResults {
fetchAndReplaceArticlesAsync()
}
else {
} else {
fetchAndReplaceArticlesSync()
if articles.count > 0 {
tableView.scrollRowToVisible(0)
@@ -85,9 +79,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
defer {
updateUnreadCount()
}
if articles == oldValue {
return
}
if articles.representSameArticlesInSameOrder(as: oldValue) {
// When the array is the same  same articles, same order
// but some data in some of the articles may have changed.
@@ -96,7 +92,20 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
reloadVisibleCells()
return
}
updateShowIcons()
if let representedObjects = representedObjects, representedObjects.count == 1 && representedObjects.first is WebFeed {
showFeedNames = {
for article in articles {
if !article.byline().isEmpty {
return .byline
}
}
return .none
}()
} else {
showFeedNames = .feed
}
articleRowMap = [String: Int]()
tableView.reloadData()
}
@@ -117,7 +126,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
private var articleRowMap = [String: Int]() // articleID: rowIndex
private var cellAppearance: TimelineCellAppearance!
private var cellAppearanceWithIcon: TimelineCellAppearance!
private var showFeedNames = false {
private var showFeedNames: TimelineShowFeedName = .none {
didSet {
if showFeedNames != oldValue {
updateShowIcons()
@@ -663,7 +672,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: true, feedName: "Prototype Feed Name", iconImage: nil, showIcon: false, featuredImage: nil)
let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil)
let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance)
return height
}
@@ -810,7 +819,7 @@ extension TimelineViewController: NSTableViewDelegate {
private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) {
cell.objectValue = article
let iconImage = article.iconImage()
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.webFeed?.nameForDisplay, iconImage: iconImage, showIcon: showIcons, featuredImage: nil)
cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.webFeed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcons, featuredImage: nil)
}
private func iconFor(_ article: Article) -> IconImage? {
@@ -946,20 +955,25 @@ private extension TimelineViewController {
}
func updateShowIcons() {
if showFeedNames {
if showFeedNames == .feed {
self.showIcons = true
return
}
if showFeedNames == .none {
self.showIcons = false
return
}
for article in articles {
if let authors = article.authors {
for author in authors {
if author.avatarURL != nil {
self.showIcons = true
return
for author in authors {
if author.avatarURL != nil {
self.showIcons = true
return
}
}
}
}
}
self.showIcons = false