Update the timeline cell when an article’s status changes.

This commit is contained in:
Brent Simmons 2017-10-08 21:06:25 -07:00
parent e66e6083c7
commit 6572631866
11 changed files with 118 additions and 97 deletions

View File

@ -12,13 +12,13 @@ import Account
// These handle multiple accounts. // These handle multiple accounts.
func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) { func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) {
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles) let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
d.keys.forEach { (accountID) in for (accountID, accountArticles) in d {
guard let accountArticles = d[accountID], let account = AccountManager.shared.existingAccount(with: accountID) else { guard let account = AccountManager.shared.existingAccount(with: accountID) else {
return return
} }
@ -28,17 +28,8 @@ func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) {
private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String: Set<Article>] { private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String: Set<Article>] {
var d = [String: Set<Article>]() let d = Dictionary(grouping: articles, by: { $0.accountID })
return d.mapValues{ Set($0) }
articles.forEach { (article) in
let accountID = article.accountID
var articleSet: Set<Article> = d[accountID] ?? Set<Article>()
articleSet.insert(article)
d[accountID] = articleSet
}
return d
} }
extension Article { extension Article {

View File

@ -76,7 +76,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
if !didRegisterForNotifications { if !didRegisterForNotifications {
NotificationCenter.default.addObserver(self, selector: #selector(sidebarSelectionDidChange(_:)), name: .SidebarSelectionDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sidebarSelectionDidChange(_:)), name: .SidebarSelectionDidChange, object: nil)
// NotificationCenter.default.addObserver(self, selector: #selector(articleStatusesDidChange(_:)), name: .ArticleStatusesDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
NSUserDefaultsController.shared.addObserver(self, forKeyPath: timelineFontSizeKVOKey, options: NSKeyValueObservingOptions(rawValue: 0), context: nil) NSUserDefaultsController.shared.addObserver(self, forKeyPath: timelineFontSizeKVOKey, options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
@ -115,7 +115,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
} }
} }
// MARK: API // MARK: - API
func markAllAsRead() { func markAllAsRead() {
@ -123,12 +123,12 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return return
} }
markArticles(Set(articles), statusKey: ArticleStatusKey.read.rawValue, flag: true) markArticles(Set(articles), statusKey: .read, flag: true)
reloadCellsForArticles(articles) reloadCellsForArticles(articles)
} }
// MARK: Actions // MARK: - Actions
@objc func openArticleInBrowser(_ sender: AnyObject) { @objc func openArticleInBrowser(_ sender: AnyObject) {
@ -146,20 +146,20 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
let status = articles.first!.status let status = articles.first!.status
let markAsRead = !status.read let markAsRead = !status.read
markArticles(Set(articles), statusKey: ArticleStatusKey.read.rawValue, flag: markAsRead) markArticles(Set(articles), statusKey: .read, flag: markAsRead)
} }
@IBAction func markSelectedArticlesAsRead(_ sender: AnyObject) { @IBAction func markSelectedArticlesAsRead(_ sender: AnyObject) {
markArticles(Set(selectedArticles), statusKey: ArticleStatusKey.read.rawValue, flag: true) markArticles(Set(selectedArticles), statusKey: .read, flag: true)
} }
@IBAction func markSelectedArticlesAsUnread(_ sender: AnyObject) { @IBAction func markSelectedArticlesAsUnread(_ sender: AnyObject) {
markArticles(Set(selectedArticles), statusKey: ArticleStatusKey.read.rawValue, flag: false) markArticles(Set(selectedArticles), statusKey: .read, flag: false)
} }
// MARK: Navigation // MARK: - Navigation
func goToNextUnread() { func goToNextUnread() {
@ -212,7 +212,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return nil return nil
} }
// MARK: Notifications // MARK: - Notifications
@objc func sidebarSelectionDidChange(_ note: Notification) { @objc func sidebarSelectionDidChange(_ note: Notification) {
@ -223,12 +223,12 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
} }
} }
@objc func articleStatusesDidChange(_ note: Notification) { @objc func statusesDidChange(_ note: Notification) {
guard let articles = note.appInfo?.articles else { guard let statuses = note.userInfo?[Account.UserInfoKey.statuses] as? Set<ArticleStatus> else {
return return
} }
reloadCellsForArticles(Array(articles)) reloadCellsForArticleIDs(statuses.articleIDs())
} }
func fontSizeInDefaultsDidChange() { func fontSizeInDefaultsDidChange() {
@ -243,7 +243,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
} }
} }
// MARK: KeyboardDelegate // MARK: - KeyboardDelegate
func handleKeydownEvent(_ event: NSEvent, sender: AnyObject) -> Bool { func handleKeydownEvent(_ event: NSEvent, sender: AnyObject) -> Bool {
@ -302,7 +302,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return keyHandled return keyHandled
} }
// MARK: Reloading Data // MARK: - Reloading Data
private func cellForRowView(_ rowView: NSView) -> NSView? { private func cellForRowView(_ rowView: NSView) -> NSView? {
@ -314,18 +314,23 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
private func reloadCellsForArticles(_ articles: [Article]) { private func reloadCellsForArticles(_ articles: [Article]) {
let indexes = indexesForArticles(articles) reloadCellsForArticleIDs(Set(articles.articleIDs()))
}
private func reloadCellsForArticleIDs(_ articleIDs: Set<String>) {
let indexes = indexesForArticleIDs(articleIDs)
tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet) tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet)
} }
// MARK: Articles // MARK: - Articles
private func indexesForArticles(_ articles: [Article]) -> IndexSet { private func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet() var indexes = IndexSet()
articles.forEach { (article) in articleIDs.forEach { (articleID) in
let oneIndex = rowForArticle(article) let oneIndex = rowForArticleID(articleID)
if oneIndex != NSNotFound { if oneIndex != NSNotFound {
indexes.insert(oneIndex) indexes.insert(oneIndex)
} }
@ -351,7 +356,12 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
private func rowForArticle(_ article: Article) -> Int { private func rowForArticle(_ article: Article) -> Int {
if let index = articles.index(where: { $0.articleID == article.articleID }) { return rowForArticleID(article.articleID)
}
private func rowForArticleID(_ articleID: String) -> Int {
if let index = articles.index(where: { $0.articleID == articleID }) {
return index return index
} }
@ -409,7 +419,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
} }
} }
// MARK: Cell Configuring // MARK: - Cell Configuring
private func calculateRowHeight() -> CGFloat { private func calculateRowHeight() -> CGFloat {
@ -435,7 +445,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
cell.cellData = emptyCellData cell.cellData = emptyCellData
} }
// MARK: NSTableViewDataSource // MARK: - NSTableViewDataSource
func numberOfRows(in tableView: NSTableView) -> Int { func numberOfRows(in tableView: NSTableView) -> Int {
@ -447,7 +457,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return articleAtRow(row) return articleAtRow(row)
} }
// MARK: NSTableViewDelegate // MARK: - NSTableViewDelegate
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
@ -495,7 +505,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
if let selectedArticle = articleAtRow(selectedRow) { if let selectedArticle = articleAtRow(selectedRow) {
if (!selectedArticle.status.read) { if (!selectedArticle.status.read) {
markArticles(Set([selectedArticle]), statusKey: ArticleStatusKey.read.rawValue, flag: true) markArticles(Set([selectedArticle]), statusKey: .read, flag: true)
} }
postTimelineSelectionDidChangeNotification(selectedArticle) postTimelineSelectionDidChangeNotification(selectedArticle)
} }
@ -536,6 +546,8 @@ private extension TimelineViewController {
} }
} }
// MARK: - NSTableView extension
private extension NSTableView { private extension NSTableView {
func scrollTo(row: Int) { func scrollTo(row: Int) {
@ -593,3 +605,4 @@ private extension NSTableView {
visibleRowViews()?.forEach { $0.invalidateGridRect() } visibleRowViews()?.forEach { $0.invalidateGridRect() }
} }
} }

View File

@ -19,6 +19,8 @@ public extension Notification.Name {
public static let AccountRefreshDidFinish = Notification.Name(rawValue: "AccountRefreshDidFinish") public static let AccountRefreshDidFinish = Notification.Name(rawValue: "AccountRefreshDidFinish")
public static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange") public static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange")
public static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles") public static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
public static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
} }
public enum AccountType: Int { public enum AccountType: Int {
@ -34,9 +36,10 @@ public enum AccountType: Int {
public final class Account: DisplayNameProvider, Container, Hashable { public final class Account: DisplayNameProvider, Container, Hashable {
public struct UserInfoKey { // Used by AccountDidDownloadArticles. public struct UserInfoKey {
public static let newArticles = "newArticles" public static let newArticles = "newArticles" // AccountDidDownloadArticles
public static let updatedArticles = "updatedArticles" public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
public static let statuses = "statuses" // ArticleStatusesDidChange
} }
public let accountID: String public let accountID: String
@ -151,9 +154,11 @@ public final class Account: DisplayNameProvider, Container, Hashable {
} }
} }
public func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) { public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) {
database.mark(articles, statusKey: statusKey, flag: flag) if let updatedStatuses = database.mark(articles, statusKey: statusKey, flag: flag) {
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: updatedStatuses])
}
} }
public func ensureFolder(with name: String) -> Folder? { public func ensureFolder(with name: String) -> Folder? {

View File

@ -71,4 +71,18 @@ public struct Article: Hashable {
} }
} }
public extension Set where Element == Article {
public func articleIDs() -> Set<String> {
return Set<String>(map { $0.articleID })
}
}
public extension Array where Element == Article {
public func articleIDs() -> [String] {
return map { $0.articleID }
}
}

View File

@ -14,15 +14,14 @@ import Foundation
// Which is safe, because at creation time itt not yet shared, // Which is safe, because at creation time itt not yet shared,
// and it wont be mutated ever on a background thread. // and it wont be mutated ever on a background thread.
public enum ArticleStatusKey: String { public final class ArticleStatus: Hashable {
public enum Key: String {
case read = "read" case read = "read"
case starred = "starred" case starred = "starred"
case userDeleted = "userDeleted" case userDeleted = "userDeleted"
} }
public final class ArticleStatus: Hashable {
public let articleID: String public let articleID: String
public let dateArrived: Date public let dateArrived: Date
public let hashValue: Int public let hashValue: Int
@ -46,10 +45,9 @@ public final class ArticleStatus: Hashable {
self.init(articleID: articleID, read: false, starred: false, userDeleted: false, dateArrived: dateArrived) self.init(articleID: articleID, read: false, starred: false, userDeleted: false, dateArrived: dateArrived)
} }
public func boolStatus(forKey key: String) -> Bool { public func boolStatus(forKey key: ArticleStatus.Key) -> Bool {
if let articleStatusKey = ArticleStatusKey(rawValue: key) { switch key {
switch articleStatusKey {
case .read: case .read:
return read return read
case .starred: case .starred:
@ -58,13 +56,10 @@ public final class ArticleStatus: Hashable {
return userDeleted return userDeleted
} }
} }
return false
}
public func setBoolStatus(_ status: Bool, forKey key: String) { public func setBoolStatus(_ status: Bool, forKey key: ArticleStatus.Key) {
if let articleStatusKey = ArticleStatusKey(rawValue: key) { switch key {
switch articleStatusKey {
case .read: case .read:
read = status read = status
case .starred: case .starred:
@ -73,10 +68,25 @@ public final class ArticleStatus: Hashable {
userDeleted = status userDeleted = status
} }
} }
}
public static func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool { public static func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool {
return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred && lhs.userDeleted == rhs.userDeleted return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred && lhs.userDeleted == rhs.userDeleted
} }
} }
public extension Set where Element == ArticleStatus {
public func articleIDs() -> Set<String> {
return Set<String>(map { $0.articleID })
}
}
public extension Array where Element == ArticleStatus {
public func articleIDs() -> [String] {
return map { $0.articleID }
}
}

View File

@ -144,9 +144,9 @@ final class ArticlesTable: DatabaseTable {
// MARK: Status // MARK: Status
func mark(_ articles: Set<Article>, _ statusKey: String, _ flag: Bool) { func mark(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<ArticleStatus>? {
statusesTable.mark(articles.statuses(), statusKey, flag) return statusesTable.mark(articles.statuses(), statusKey, flag)
} }
} }

View File

@ -70,9 +70,9 @@ public final class Database {
// MARK: - Status // MARK: - Status
public func mark(_ articles: Set<Article>, statusKey: String, flag: Bool) { public func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<ArticleStatus>? {
articlesTable.mark(articles, statusKey, flag) return articlesTable.mark(articles, statusKey, flag)
} }
} }

View File

@ -118,11 +118,6 @@ extension Article: DatabaseObject {
extension Set where Element == Article { extension Set where Element == Article {
func articleIDs() -> Set<String> {
return Set<String>(map { $0.databaseID })
}
func statuses() -> Set<ArticleStatus> { func statuses() -> Set<ArticleStatus> {
return Set<ArticleStatus>(map { $0.status }) return Set<ArticleStatus>(map { $0.status })

View File

@ -45,10 +45,3 @@ extension ArticleStatus: DatabaseObject {
} }
} }
extension Set where Element == ArticleStatus {
func articleIDs() -> Set<String> {
return Set<String>(map { $0.articleID })
}
}

View File

@ -50,7 +50,7 @@ final class StatusesTable: DatabaseTable {
// MARK: Marking // MARK: Marking
func mark(_ statuses: Set<ArticleStatus>, _ statusKey: String, _ flag: Bool) { func mark(_ statuses: Set<ArticleStatus>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<ArticleStatus>? {
// Sets flag in both memory and in database. // Sets flag in both memory and in database.
@ -66,13 +66,14 @@ final class StatusesTable: DatabaseTable {
} }
if updatedStatuses.isEmpty { if updatedStatuses.isEmpty {
return return nil
} }
let articleIDs = updatedStatuses.articleIDs() let articleIDs = updatedStatuses.articleIDs()
queue.update { (database) in queue.update { (database) in
self.markArticleIDs(articleIDs, statusKey, flag, database) self.markArticleIDs(articleIDs, statusKey, flag, database)
} }
return updatedStatuses
} }
// MARK: Fetching // MARK: Fetching
@ -150,9 +151,9 @@ private extension StatusesTable {
// MARK: Marking // MARK: Marking
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: String, _ flag: Bool, _ database: FMDatabase) { func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database) updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey.rawValue, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database)
} }
} }

View File

@ -6,7 +6,7 @@
</editor> --> </editor> -->
<title>ToDo</title> <title>ToDo</title>
<dateCreated>Tue, 12 Sep 2017 20:15:17 GMT</dateCreated> <dateCreated>Tue, 12 Sep 2017 20:15:17 GMT</dateCreated>
<expansionState>0,1,23,24,27,31,37,45,46,48,63,68</expansionState> <expansionState>0,23,24,27,31,37,45,46,48,63,68</expansionState>
<vertScrollState>0</vertScrollState> <vertScrollState>0</vertScrollState>
<windowTop>637</windowTop> <windowTop>637</windowTop>
<windowLeft>42</windowLeft> <windowLeft>42</windowLeft>
@ -15,9 +15,8 @@
</head> </head>
<body> <body>
<outline text="App"> <outline text="App">
<outline text="Mark article as read on selection"> <outline text="Make adding a Folder work — accounts dont show up in Accounts popup"/>
<outline text="Update display"/> <outline text="Update unread count on marking article status"/>
</outline>
<outline text="Update Sparkle"/> <outline text="Update Sparkle"/>
<outline text="Use new app icon"/> <outline text="Use new app icon"/>
<outline text="Set -NSApplicationCrashOnExceptions YES"/> <outline text="Set -NSApplicationCrashOnExceptions YES"/>