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.
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)
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
}
@ -28,17 +28,8 @@ func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) {
private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String: Set<Article>] {
var d = [String: Set<Article>]()
articles.forEach { (article) in
let accountID = article.accountID
var articleSet: Set<Article> = d[accountID] ?? Set<Article>()
articleSet.insert(article)
d[accountID] = articleSet
}
return d
let d = Dictionary(grouping: articles, by: { $0.accountID })
return d.mapValues{ Set($0) }
}
extension Article {

View File

@ -76,7 +76,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
if !didRegisterForNotifications {
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)
@ -115,7 +115,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
}
// MARK: API
// MARK: - API
func markAllAsRead() {
@ -123,12 +123,12 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return
}
markArticles(Set(articles), statusKey: ArticleStatusKey.read.rawValue, flag: true)
markArticles(Set(articles), statusKey: .read, flag: true)
reloadCellsForArticles(articles)
}
// MARK: Actions
// MARK: - Actions
@objc func openArticleInBrowser(_ sender: AnyObject) {
@ -146,20 +146,20 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
let status = articles.first!.status
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) {
markArticles(Set(selectedArticles), statusKey: ArticleStatusKey.read.rawValue, flag: true)
markArticles(Set(selectedArticles), statusKey: .read, flag: true)
}
@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() {
@ -212,7 +212,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return nil
}
// MARK: Notifications
// MARK: - Notifications
@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
}
reloadCellsForArticles(Array(articles))
reloadCellsForArticleIDs(statuses.articleIDs())
}
func fontSizeInDefaultsDidChange() {
@ -243,7 +243,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
}
// MARK: KeyboardDelegate
// MARK: - KeyboardDelegate
func handleKeydownEvent(_ event: NSEvent, sender: AnyObject) -> Bool {
@ -302,7 +302,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return keyHandled
}
// MARK: Reloading Data
// MARK: - Reloading Data
private func cellForRowView(_ rowView: NSView) -> NSView? {
@ -314,18 +314,23 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
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)
}
// MARK: Articles
// MARK: - Articles
private func indexesForArticles(_ articles: [Article]) -> IndexSet {
private func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet()
articles.forEach { (article) in
let oneIndex = rowForArticle(article)
articleIDs.forEach { (articleID) in
let oneIndex = rowForArticleID(articleID)
if oneIndex != NSNotFound {
indexes.insert(oneIndex)
}
@ -351,7 +356,12 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
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
}
@ -409,7 +419,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
}
}
// MARK: Cell Configuring
// MARK: - Cell Configuring
private func calculateRowHeight() -> CGFloat {
@ -435,7 +445,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
cell.cellData = emptyCellData
}
// MARK: NSTableViewDataSource
// MARK: - NSTableViewDataSource
func numberOfRows(in tableView: NSTableView) -> Int {
@ -447,7 +457,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
return articleAtRow(row)
}
// MARK: NSTableViewDelegate
// MARK: - NSTableViewDelegate
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
@ -495,7 +505,7 @@ class TimelineViewController: NSViewController, NSTableViewDelegate, NSTableView
if let selectedArticle = articleAtRow(selectedRow) {
if (!selectedArticle.status.read) {
markArticles(Set([selectedArticle]), statusKey: ArticleStatusKey.read.rawValue, flag: true)
markArticles(Set([selectedArticle]), statusKey: .read, flag: true)
}
postTimelineSelectionDidChangeNotification(selectedArticle)
}
@ -536,6 +546,8 @@ private extension TimelineViewController {
}
}
// MARK: - NSTableView extension
private extension NSTableView {
func scrollTo(row: Int) {
@ -593,3 +605,4 @@ private extension NSTableView {
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 AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange")
public static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
public static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
}
public enum AccountType: Int {
@ -34,9 +36,10 @@ public enum AccountType: Int {
public final class Account: DisplayNameProvider, Container, Hashable {
public struct UserInfoKey { // Used by AccountDidDownloadArticles.
public static let newArticles = "newArticles"
public static let updatedArticles = "updatedArticles"
public struct UserInfoKey {
public static let newArticles = "newArticles" // AccountDidDownloadArticles
public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
public static let statuses = "statuses" // ArticleStatusesDidChange
}
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? {

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,
// 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 starred = "starred"
case userDeleted = "userDeleted"
}
public final class ArticleStatus: Hashable {
public let articleID: String
public let dateArrived: Date
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)
}
public func boolStatus(forKey key: String) -> Bool {
public func boolStatus(forKey key: ArticleStatus.Key) -> Bool {
if let articleStatusKey = ArticleStatusKey(rawValue: key) {
switch articleStatusKey {
switch key {
case .read:
return read
case .starred:
@ -58,13 +56,10 @@ public final class ArticleStatus: Hashable {
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 articleStatusKey {
switch key {
case .read:
read = status
case .starred:
@ -73,10 +68,25 @@ public final class ArticleStatus: Hashable {
userDeleted = status
}
}
}
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
}
}
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
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
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 {
func articleIDs() -> Set<String> {
return Set<String>(map { $0.databaseID })
}
func statuses() -> Set<ArticleStatus> {
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
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.
@ -66,13 +66,14 @@ final class StatusesTable: DatabaseTable {
}
if updatedStatuses.isEmpty {
return
return nil
}
let articleIDs = updatedStatuses.articleIDs()
queue.update { (database) in
self.markArticleIDs(articleIDs, statusKey, flag, database)
}
return updatedStatuses
}
// MARK: Fetching
@ -150,9 +151,9 @@ private extension StatusesTable {
// 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> -->
<title>ToDo</title>
<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>
<windowTop>637</windowTop>
<windowLeft>42</windowLeft>
@ -15,9 +15,8 @@
</head>
<body>
<outline text="App">
<outline text="Mark article as read on selection">
<outline text="Update display"/>
</outline>
<outline text="Make adding a Folder work — accounts dont show up in Accounts popup"/>
<outline text="Update unread count on marking article status"/>
<outline text="Update Sparkle"/>
<outline text="Use new app icon"/>
<outline text="Set -NSApplicationCrashOnExceptions YES"/>