Merge pull request #1016 from philviso/GroupArticlesByFeed
Add setting to group articles by feed in timeline
This commit is contained in:
commit
1bcd265c80
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue