Merge pull request #1016 from philviso/GroupArticlesByFeed

Add setting to group articles by feed in timeline
This commit is contained in:
Maurice Parker 2019-09-13 10:58:17 -05:00 committed by GitHub
commit 1bcd265c80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 432 additions and 32 deletions

View File

@ -22,6 +22,7 @@ struct AppDefaults {
static let sidebarFontSize = "sidebarFontSize"
static let timelineFontSize = "timelineFontSize"
static let timelineSortDirection = "timelineSortDirection"
static let timelineGroupByFeed = "timelineGroupByFeed"
static let detailFontSize = "detailFontSize"
static let openInBrowserInBackground = "openInBrowserInBackground"
static let mainWindowWidths = "mainWindowWidths"
@ -137,6 +138,15 @@ struct AppDefaults {
}
}
static var timelineGroupByFeed: Bool {
get {
return bool(for: Key.timelineGroupByFeed)
}
set {
setBool(for: Key.timelineGroupByFeed, newValue)
}
}
static var timelineShowsSeparators: Bool {
return bool(for: Key.timelineShowsSeparators)
}
@ -161,7 +171,13 @@ struct AppDefaults {
}
static func registerDefaults() {
let defaults: [String : Any] = [Key.sidebarFontSize: FontSize.medium.rawValue, Key.timelineFontSize: FontSize.medium.rawValue, Key.detailFontSize: FontSize.medium.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, "NSScrollViewShouldScrollUnderTitlebar": false, Key.refreshInterval: RefreshInterval.everyHour.rawValue]
let defaults: [String : Any] = [Key.sidebarFontSize: FontSize.medium.rawValue,
Key.timelineFontSize: FontSize.medium.rawValue,
Key.detailFontSize: FontSize.medium.rawValue,
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
Key.timelineGroupByFeed: false,
"NSScrollViewShouldScrollUnderTitlebar": false,
Key.refreshInterval: RefreshInterval.everyHour.rawValue]
UserDefaults.standard.register(defaults: defaults)

View File

@ -41,6 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@IBOutlet var debugMenuItem: NSMenuItem!
@IBOutlet var sortByOldestArticleOnTopMenuItem: NSMenuItem!
@IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem!
@IBOutlet var groupArticlesByFeedMenuItem: NSMenuItem!
@IBOutlet var checkForUpdatesMenuItem: NSMenuItem!
var unreadCount = 0 {
@ -148,6 +149,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
feedIconDownloader = FeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
updateSortMenuItems()
updateGroupByFeedMenuItem()
createAndShowMainWindow()
if isFirstRun {
mainWindowController?.window?.center()
@ -259,6 +261,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@objc func userDefaultsDidChange(_ note: Notification) {
updateSortMenuItems()
updateGroupByFeedMenuItem()
refreshTimer?.update()
updateDockBadge()
}
@ -509,6 +512,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
AppDefaults.timelineSortDirection = .orderedDescending
}
@IBAction func groupByFeedToggled(_ sender: NSMenuItem) {
AppDefaults.timelineGroupByFeed.toggle()
}
}
// MARK: - Debug Menu
@ -546,6 +554,11 @@ private extension AppDelegate {
sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off
sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on
}
func updateGroupByFeedMenuItem() {
let groupByFeedEnabled = AppDefaults.timelineGroupByFeed
groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off
}
}
/*

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14865.1"/>
</dependencies>
<scenes>
<!--Application-->
@ -330,9 +331,9 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Sort By" id="nLP-fa-KUi">
<menuItem title="Sort Articles By" id="nLP-fa-KUi">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Sort By" id="OlJ-93-6OP">
<menu key="submenu" title="Sort Articles By" id="OlJ-93-6OP">
<items>
<menuItem title="Newest Article on Top" id="TNS-TV-n0U">
<modifierMask key="keyEquivalentModifierMask"/>
@ -349,6 +350,12 @@
</items>
</menu>
</menuItem>
<menuItem title="Group By Feed" id="Zxm-O6-NRE">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="groupByFeedToggled:" target="Voe-Tx-rLC" id="Wxz-eM-hJN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="dZt-2W-gxf"/>
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
@ -581,6 +588,7 @@
<connections>
<outlet property="checkForUpdatesMenuItem" destination="1nF-7O-aKU" id="JmT-jc-DJ8"/>
<outlet property="debugMenuItem" destination="UqE-mp-gtV" id="OnR-lr-Zlt"/>
<outlet property="groupArticlesByFeedMenuItem" destination="Zxm-O6-NRE" id="gwn-VT-2YZ"/>
<outlet property="sortByNewestArticleOnTopMenuItem" destination="TNS-TV-n0U" id="gix-Nd-9k4"/>
<outlet property="sortByOldestArticleOnTopMenuItem" destination="iii-kP-qoF" id="fTe-Tf-EWG"/>
</connections>

View File

@ -126,7 +126,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
private var sortDirection = AppDefaults.timelineSortDirection {
didSet {
if sortDirection != oldValue {
sortDirectionDidChange()
sortParametersDidChange()
}
}
}
private var groupByFeed = AppDefaults.timelineGroupByFeed {
didSet {
if groupByFeed != oldValue {
sortParametersDidChange()
}
}
}
@ -555,6 +562,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
self.fontSize = AppDefaults.timelineFontSize
self.sortDirection = AppDefaults.timelineSortDirection
self.groupByFeed = AppDefaults.timelineGroupByFeed
}
@objc func appleInterfaceThemeChanged(_ note: Notification) {
@ -876,8 +884,7 @@ private extension TimelineViewController {
}
}
func sortDirectionDidChange() {
func sortParametersDidChange() {
performBlockAndRestoreSelection {
let unsortedArticles = Set(articles)
replaceArticles(with: unsortedArticles)
@ -980,7 +987,7 @@ private extension TimelineViewController {
}
func replaceArticles(with unsortedArticles: Set<Article>) {
articles = Array(unsortedArticles).sortedByDate(sortDirection)
articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
}
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {

View File

@ -366,6 +366,9 @@
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; };
DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; };
DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF999FF622B5AEFA0064B687 /* SafariView.swift */; };
FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; };
FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -1046,6 +1049,8 @@
D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = "<group>"; };
DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = "<group>"; };
DF999FF622B5AEFA0064B687 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = "<group>"; };
FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -1354,8 +1359,9 @@
isa = PBXGroup;
children = (
84F204DF1FAACBB30076E152 /* ArticleArray.swift */,
84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */,
FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */,
84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */,
84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */,
849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */,
);
path = Timeline;
@ -1979,6 +1985,7 @@
isa = PBXGroup;
children = (
84F9EAD0213660A100CF2DE4 /* ScriptingTests */,
FF3ABF09232599450074C542 /* ArticleSorterTests.swift */,
84F9EAE3213660A100CF2DE4 /* NetNewsWireTests.swift */,
DD82AB09231003F6002269DF /* SharingTests.swift */,
84F9EAE4213660A100CF2DE4 /* Info.plist */,
@ -2643,6 +2650,7 @@
51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */,
514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */,
5152E0F923248F6200E5C7AD /* SettingsLocalAccountView.swift in Sources */,
FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */,
51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */,
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */,
@ -2777,6 +2785,7 @@
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */,
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */,
FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */,
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */,
849A97791ED9EC04007D329B /* TimelineStringFormatter.swift in Sources */,
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
@ -2855,6 +2864,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */,
DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */,
84F9EAEB213660A100CF2DE4 /* testIterativeCreateAndDeleteFeed.applescript in Sources */,
84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */,

View File

@ -90,3 +90,25 @@ extension Article {
}
}
// MARK: SortableArticle
extension Article: SortableArticle {
var sortableName: String {
return feed?.name ?? ""
}
var sortableDate: Date {
return logicalDatePublished
}
var sortableArticleID: String {
return articleID
}
var sortableFeedID: String {
return feedID
}
}

View File

@ -50,19 +50,8 @@ extension Array where Element == Article {
})
}
func sortedByDate(_ sortDirection: ComparisonResult) -> ArticleArray {
let articles = sorted { (article1, article2) -> Bool in
if article1.logicalDatePublished == article2.logicalDatePublished {
return article1.articleID < article2.articleID
}
if sortDirection == .orderedDescending {
return article1.logicalDatePublished > article2.logicalDatePublished
}
return article1.logicalDatePublished < article2.logicalDatePublished
}
return articles
func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray {
return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed)
}
func canMarkAllAsRead() -> Bool {

View File

@ -0,0 +1,61 @@
//
// ArticleSorter.swift
// NetNewsWire
//
// Created by Phil Viso on 9/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Articles
import Foundation
protocol SortableArticle {
var sortableName: String { get }
var sortableDate: Date { get }
var sortableArticleID: String { get }
var sortableFeedID: String { get }
}
struct ArticleSorter {
static func sortedByDate<T: SortableArticle>(articles: [T],
sortDirection: ComparisonResult,
groupByFeed: Bool) -> [T] {
if groupByFeed {
return sortedByFeedName(articles: articles, sortByDateDirection: sortDirection)
} else {
return sortedByDate(articles: articles, sortDirection: sortDirection)
}
}
// MARK: -
private static func sortedByFeedName<T: SortableArticle>(articles: [T],
sortByDateDirection: ComparisonResult) -> [T] {
// Group articles by "feed-feedID" - feed ID is used to differentiate between
// two feeds that have the same name
let groupedArticles = Dictionary(grouping: articles) { "\($0.sortableName.lowercased())-\($0.sortableFeedID)" }
return groupedArticles
.sorted { $0.key < $1.key }
.flatMap { (tuple) -> [T] in
let (_, articles) = tuple
return sortedByDate(articles: articles, sortDirection: sortByDateDirection)
}
}
private static func sortedByDate<T: SortableArticle>(articles: [T],
sortDirection: ComparisonResult) -> [T] {
return articles.sorted { (article1, article2) -> Bool in
if article1.sortableDate == article2.sortableDate {
return article1.sortableArticleID < article2.sortableArticleID
}
if sortDirection == .orderedDescending {
return article1.sortableDate > article2.sortableDate
}
return article1.sortableDate < article2.sortableDate
}
}
}

View File

@ -0,0 +1,240 @@
//
// ArticleSorterTests.swift
// NetNewsWire
//
// Created by Phil Viso on 9/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Articles
import Foundation
import XCTest
@testable import NetNewsWire
class ArticleSorterTests: XCTestCase {
// MARK: sortByDate ascending tests
func testSortByDateAscending() {
let now = Date()
let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableFeedID: "4")
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableFeedID: "6")
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableFeedID: "6")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableFeedID: "5")
let articles = [article1, article2, article3, article4]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: false)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article4)
XCTAssertEqual(sortedArticles.articleAtRow(1), article1)
XCTAssertEqual(sortedArticles.articleAtRow(2), article2)
XCTAssertEqual(sortedArticles.articleAtRow(3), article3)
}
func testSortByDateAscendingWithSameDate() {
let now = Date()
// Articles with the same date should end up being sorted by their article ID
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "3")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableFeedID: "4")
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableFeedID: "5")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: false)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article5)
XCTAssertEqual(sortedArticles.articleAtRow(1), article4)
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article3)
}
func testSortByDateAscendingWithGroupByFeed() {
let now = Date()
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "2")
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableFeedID: "3")
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "3")
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableFeedID: "2")
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "0")
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "0")
let articles = [article1, article2, article3, article4, article5, article6, article7, article8, article9]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles, sortDirection: .orderedAscending, groupByFeed: true)
XCTAssertEqual(sortedArticles.count, 9)
// Gordy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(0), article4)
XCTAssertEqual(sortedArticles.articleAtRow(1), article5)
// Jenny's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(2), article3)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article6)
// Phil's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(5), article1)
XCTAssertEqual(sortedArticles.articleAtRow(6), article7)
// Zippy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(7), article8)
XCTAssertEqual(sortedArticles.articleAtRow(8), article9)
}
// MARK: sortByDate descending tests
func testSortByDateDescending() {
let now = Date()
let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableFeedID: "4")
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableFeedID: "6")
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableFeedID: "6")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableFeedID: "5")
let articles = [article1, article2, article3, article4]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedDescending,
groupByFeed: false)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article3)
XCTAssertEqual(sortedArticles.articleAtRow(1), article2)
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article4)
}
func testSortByDateDescendingWithSameDate() {
let now = Date()
// Articles with the same date should end up being sorted by their article ID
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "3")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableFeedID: "4")
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableFeedID: "5")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedDescending,
groupByFeed: false)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article1)
XCTAssertEqual(sortedArticles.articleAtRow(1), article2)
XCTAssertEqual(sortedArticles.articleAtRow(2), article3)
XCTAssertEqual(sortedArticles.articleAtRow(3), article4)
XCTAssertEqual(sortedArticles.articleAtRow(4), article5)
}
func testSortByDateDescendingWithGroupByFeed() {
let now = Date()
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "2")
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableFeedID: "3")
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "3")
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableFeedID: "2")
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "0")
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "0")
let articles = [article1, article2, article3, article4, article5, article6, article7, article8, article9]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles, sortDirection: .orderedDescending, groupByFeed: true)
XCTAssertEqual(sortedArticles.count, 9)
// Gordy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(0), article5)
XCTAssertEqual(sortedArticles.articleAtRow(1), article4)
// Jenny's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(2), article6)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article3)
// Phil's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(5), article7)
XCTAssertEqual(sortedArticles.articleAtRow(6), article1)
// Zippy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(7), article8)
XCTAssertEqual(sortedArticles.articleAtRow(8), article9)
}
// MARK: Additional group by feed tests
func testGroupByFeedWithCaseInsensitiveFeedNames() {
let now = Date()
let article1 = TestArticle(sortableName: "phil's feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "PhIl's FEed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
let article3 = TestArticle(sortableName: "APPLE's feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "2")
let article4 = TestArticle(sortableName: "PHIL'S FEED", sortableDate: now, sortableArticleID: "4", sortableFeedID: "1")
let article5 = TestArticle(sortableName: "apple's feed", sortableDate: now, sortableArticleID: "5", sortableFeedID: "2")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: true)
XCTAssertEqual(sortedArticles.count, articles.count)
// Apple's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(0), article3)
XCTAssertEqual(sortedArticles.articleAtRow(1), article5)
// Phil's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article4)
}
func testGroupByFeedWithSameFeedNames() {
let now = Date()
// Articles with the same feed name should be sorted by feed ID
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "1")
let article4 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "4", sortableFeedID: "2")
let article5 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "5", sortableFeedID: "1")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: true)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article3)
XCTAssertEqual(sortedArticles.articleAtRow(1), article5)
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article4)
}
}
private struct TestArticle: SortableArticle, Equatable {
let sortableName: String
let sortableDate: Date
let sortableArticleID: String
let sortableFeedID: String
}
private extension Array where Element == TestArticle {
func articleAtRow(_ row: Int) -> TestArticle? {
if row < 0 || row == NSNotFound || row > count - 1 {
return nil
}
return self[row]
}
}

View File

@ -12,10 +12,11 @@ struct AppDefaults {
struct Key {
static let firstRunDate = "firstRunDate"
static let timelineGroupByFeed = "timelineGroupByFeed"
static let timelineNumberOfLines = "timelineNumberOfLines"
static let timelineSortDirection = "timelineSortDirection"
static let refreshInterval = "refreshInterval"
static let lastRefresh = "lastRefresh"
static let timelineNumberOfLines = "timelineNumberOfLines"
}
static let isFirstRun: Bool = {
@ -36,6 +37,15 @@ struct AppDefaults {
}
}
static var timelineGroupByFeed: Bool {
get {
return bool(for: Key.timelineGroupByFeed)
}
set {
setBool(for: Key.timelineGroupByFeed, newValue)
}
}
static var timelineSortDirection: ComparisonResult {
get {
return sortDirection(for: Key.timelineSortDirection)
@ -64,7 +74,10 @@ struct AppDefaults {
}
static func registerDefaults() {
let defaults: [String : Any] = [Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, Key.refreshInterval: RefreshInterval.everyHour.rawValue, Key.timelineNumberOfLines: 3]
let defaults: [String : Any] = [Key.refreshInterval: RefreshInterval.everyHour.rawValue,
Key.timelineGroupByFeed: false,
Key.timelineNumberOfLines: 3,
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue]
UserDefaults.standard.register(defaults: defaults)
}

View File

@ -66,7 +66,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private(set) var sortDirection = AppDefaults.timelineSortDirection {
didSet {
if sortDirection != oldValue {
sortDirectionDidChange()
sortParametersDidChange()
}
}
}
private(set) var groupByFeed = AppDefaults.timelineGroupByFeed {
didSet {
if groupByFeed != oldValue {
sortParametersDidChange()
}
}
}
@ -400,6 +407,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
@objc func userDefaultsDidChange(_ note: Notification) {
self.sortDirection = AppDefaults.timelineSortDirection
self.groupByFeed = AppDefaults.timelineGroupByFeed
}
@objc func accountDidDownloadArticles(_ note: Notification) {
@ -1226,12 +1234,12 @@ private extension SceneCoordinator {
}
}
func sortDirectionDidChange() {
func sortParametersDidChange() {
replaceArticles(with: Set(articles), animate: true)
}
func replaceArticles(with unsortedArticles: Set<Article>, animate: Bool) {
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
if articles != sortedArticles {

View File

@ -54,7 +54,10 @@ struct SettingsView : View {
func buildTimelineSection() -> some View {
Section(header: Text("TIMELINE")) {
Toggle(isOn: $viewModel.sortOldestToNewest) {
Text("Sort Oldest to Newest")
Text("Sort Newest to Oldest")
}
Toggle(isOn: $viewModel.groupByFeed) {
Text("Group By Feed")
}
Stepper(value: $viewModel.timelineNumberOfLines, in: 2...6) {
Text("Number of Text Lines: \(viewModel.timelineNumberOfLines)")
@ -221,6 +224,16 @@ struct SettingsView : View {
}
}
var groupByFeed: Bool {
get {
return AppDefaults.timelineGroupByFeed
}
set {
objectWillChange.send()
AppDefaults.timelineGroupByFeed = newValue
}
}
var timelineNumberOfLines: Int {
get {
return AppDefaults.timelineNumberOfLines