Add update timeline articles statuses back in

This commit is contained in:
Maurice Parker 2020-07-25 13:53:46 -05:00
parent d3399e8632
commit 1de3c75d4f
7 changed files with 86 additions and 116 deletions

View File

@ -129,25 +129,25 @@ struct AddWebFeedView: View {
@ViewBuilder var folderPicker: some View {
#if os(iOS)
Picker("Folder", selection: $viewModel.selectedFolderIndex, content: {
ForEach(0..<viewModel.containers.count, id: \.self, content: { index in
if let containerName = (viewModel.containers[index] as? DisplayNameProvider)?.nameForDisplay {
if viewModel.containers[index] is Folder {
ForEach(0..<viewModel.containers.count, id: \.self, content: { position in
if let containerName = (viewModel.containers[position] as? DisplayNameProvider)?.nameForDisplay {
if viewModel.containers[position] is Folder {
HStack(alignment: .top) {
if let image = viewModel.smallIconImage(for: viewModel.containers[index]) {
if let image = viewModel.smallIconImage(for: viewModel.containers[position]) {
Image(rsImage: image)
.foregroundColor(Color("AccentColor"))
}
Text("\(containerName)")
.tag(index)
.tag(position)
}.padding(.leading, 16)
} else {
HStack(alignment: .top) {
if let image = viewModel.smallIconImage(for: viewModel.containers[index]) {
if let image = viewModel.smallIconImage(for: viewModel.containers[position]) {
Image(rsImage: image)
.foregroundColor(Color("AccentColor"))
}
Text(containerName)
.tag(index)
.tag(position)
}
}
}

View File

@ -18,7 +18,7 @@ enum TimelineItemStatus {
struct TimelineItem: Identifiable {
var id: String
var index: Int
var position: Int
var article: Article
var status: TimelineItemStatus = .showNone
@ -27,9 +27,9 @@ struct TimelineItem: Identifiable {
var byline: String
var dateTimeString: String
init(index: Int, article: Article) {
init(position: Int, article: Article) {
self.id = article.articleID
self.index = index
self.position = position
self.article = article
self.byline = article.webFeed?.nameForDisplay ?? ""
self.dateTimeString = ArticleStringFormatter.dateString(article.logicalDatePublished)
@ -38,6 +38,10 @@ struct TimelineItem: Identifiable {
updateStatus()
}
var isReadOnly: Bool {
return article.status.read == true && article.status.starred == false
}
mutating func updateStatus() {
if article.status.starred == true {
status = .showStar

View File

@ -0,0 +1,32 @@
//
// TimelineItems.swift
// NetNewsWire
//
// Created by Maurice Parker on 7/25/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
struct TimelineItems {
var index = [String: Int]()
var items = [TimelineItem]()
init() {}
subscript(key: String) -> TimelineItem? {
get {
if let position = index[key] {
return items[position]
}
return nil
}
}
mutating func append(_ item: TimelineItem) {
index[item.id] = item.position
items.append(item)
}
}

View File

@ -30,11 +30,12 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
@Published var selectedTimelineItemID: String? = nil // Don't use directly. Use selectedTimelineItemsPublisher
@Published var isReadFiltered: Bool? = nil
var timelineItemsPublisher: AnyPublisher<OrderedDictionary<String, TimelineItem>, Never>?
var timelineItemsPublisher: AnyPublisher<TimelineItems, Never>?
var articlesPublisher: AnyPublisher<[Article], Never>?
var selectedTimelineItemsPublisher: AnyPublisher<[TimelineItem], Never>?
var selectedArticlesPublisher: AnyPublisher<[Article], Never>?
var articleStatusChangePublisher: AnyPublisher<Set<String>, Never>?
var readFilterEnabledTable = [FeedIdentifier: Bool]()
var undoManager: UndoManager?
@ -45,7 +46,7 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
private var sortDirectionSubject = ReplaySubject<Bool, Never>(bufferSize: 1)
private var groupByFeedSubject = ReplaySubject<Bool, Never>(bufferSize: 1)
private var timelineItems = OrderedDictionary<String, TimelineItem>()
private var timelineItems = TimelineItems()
init(delegate: TimelineModelDelegate) {
self.delegate = delegate
@ -53,24 +54,17 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
subscribeToReadFilterChanges()
subscribeToArticleFetchChanges()
subscribeToSelectedArticleSelectionChanges()
// subscribeToArticleStatusChanges()
subscribeToArticleStatusChanges()
// subscribeToAccountDidDownloadArticles()
}
// MARK: Subscriptions
// func subscribeToArticleStatusChanges() {
// NotificationCenter.default.publisher(for: .StatusesDidChange).sink { [weak self] note in
// guard let self = self, let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
// return
// }
// articleIDs.forEach { articleID in
// if let timelineItemIndex = self.idToTimelineItemDictionary[articleID] {
// self.timelineItems[timelineItemIndex].updateStatus()
// }
// }
// }.store(in: &cancellables)
// }
func subscribeToArticleStatusChanges() {
articleStatusChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange)
.compactMap { $0.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> }
.eraseToAnyPublisher()
}
// func subscribeToAccountDidDownloadArticles() {
// NotificationCenter.default.publisher(for: .AccountDidDownloadArticles).sink { [weak self] note in
@ -131,7 +125,7 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
.combineLatest(sortDirectionPublisher, groupByPublisher)
.compactMap { [weak self] articles, sortDirection, groupBy in
let sortedArticles = Array(articles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupBy)
return self?.buildTimelineItems(articles: sortedArticles) ?? OrderedDictionary<String, TimelineItem>()
return self?.buildTimelineItems(articles: sortedArticles) ?? TimelineItems()
}
.share(replay: 1)
.eraseToAnyPublisher()
@ -145,7 +139,7 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
// Transform to articles for those that just need articles
articlesPublisher = timelineItemsPublisher!
.map { timelineItems in
timelineItems.values.values.map { $0.article }
timelineItems.items.map { $0.article }
}
.share()
.eraseToAnyPublisher()
@ -318,11 +312,10 @@ private extension TimelineModel {
return fetchedArticles
}
func buildTimelineItems(articles: [Article]) -> OrderedDictionary<String, TimelineItem> {
var items = OrderedDictionary<String, TimelineItem>()
for (index, article) in articles.enumerated() {
let item = TimelineItem(index: index, article: article)
items[item.id] = item
func buildTimelineItems(articles: [Article]) -> TimelineItems {
var items = TimelineItems()
for (position, article) in articles.enumerated() {
items.append(TimelineItem(position: position, article: article))
}
return items
}

View File

@ -11,7 +11,7 @@ import SwiftUI
struct TimelineView: View {
@EnvironmentObject private var timelineModel: TimelineModel
@State private var timelineItems = OrderedDictionary<String, TimelineItem>()
@State private var timelineItems = TimelineItems()
@State private var timelineItemFrames = [String: CGRect]()
@ViewBuilder var body: some View {
@ -38,12 +38,10 @@ struct TimelineView: View {
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
}
ScrollViewReader { scrollViewProxy in
List(timelineItems.keys, id: \.self, selection: $timelineModel.selectedTimelineItemIDs) { timelineItemID in
if let timelineItem = timelineItems[timelineItemID] {
let selected = timelineModel.selectedTimelineItemIDs.contains(timelineItem.article.articleID)
TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
.background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
}
List(timelineItems.items, selection: $timelineModel.selectedTimelineItemIDs) { timelineItem in
let selected = timelineModel.selectedTimelineItemIDs.contains(timelineItem.article.articleID)
TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
.background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
}
.onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in
for pref in preferences {
@ -69,6 +67,19 @@ struct TimelineView: View {
timelineItems = items
}
}
.onReceive(timelineModel.articleStatusChangePublisher!) { articleIDs in
articleIDs.forEach { articleID in
if let position = timelineItems.index[articleID] {
if timelineItems.items[position].isReadOnly {
withAnimation {
timelineItems.items[position].updateStatus()
}
} else {
timelineItems.items[position].updateStatus()
}
}
}
}
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
#else
ScrollViewReader { scrollViewProxy in

View File

@ -477,11 +477,8 @@
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
51C65AD524CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; };
51C65AD624CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; };
51C65AD724CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; };
51C65AD824CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; };
51C65AD924CC834F008EB3BD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */; };
51C65AFC24CCB2C9008EB3BD /* TimelineItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */; };
51C65AFD24CCB2C9008EB3BD /* TimelineItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */; };
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; };
51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
@ -2113,7 +2110,7 @@
51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; };
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = "<group>"; };
51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItems.swift; sourceTree = "<group>"; };
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = "<group>"; };
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; };
51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; };
@ -3012,6 +3009,7 @@
51919FED24AB85E400541E64 /* TimelineContainerView.swift */,
51B8BCE524C25F7C00360B00 /* TimelineContextMenu.swift */,
51919FF324AB869C00541E64 /* TimelineItem.swift */,
51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */,
514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */,
514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */,
51919FF024AB864A00541E64 /* TimelineModel.swift */,
@ -3588,7 +3586,6 @@
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */,
84411E701FE5FBFA004B527F /* SmallIconProvider.swift */,
51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */,
51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -5209,6 +5206,7 @@
51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */,
51919FF424AB869C00541E64 /* TimelineItem.swift in Sources */,
514E6C0224AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */,
51C65AFC24CCB2C9008EB3BD /* TimelineItems.swift in Sources */,
51E49A0024A91FC100B667CB /* SidebarContainerView.swift in Sources */,
5177471824B3812200EB0F74 /* IconView.swift in Sources */,
51E4995C24A875F300B667CB /* ArticleRenderer.swift in Sources */,
@ -5254,7 +5252,6 @@
51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */,
5177471024B3029400EB0F74 /* ArticleViewController.swift in Sources */,
65082A5224C72B88009FA994 /* SettingsCredentialsAccountModel.swift in Sources */,
51C65AD824CC834F008EB3BD /* OrderedDictionary.swift in Sources */,
172199C924AB228900A31D04 /* SettingsView.swift in Sources */,
51B8BCC224C25C3E00360B00 /* SidebarContextMenu.swift in Sources */,
51A8005124CC453C00F41F1D /* ReplaySubject.swift in Sources */,
@ -5373,6 +5370,7 @@
17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */,
514E6C0724AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */,
51E4994D24A8734C00B667CB /* ExtensionPointIdentifer.swift in Sources */,
51C65AFD24CCB2C9008EB3BD /* TimelineItems.swift in Sources */,
51B54A6724B549FE0014348B /* ArticleIconSchemeHandler.swift in Sources */,
51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */,
51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */,
@ -5393,7 +5391,6 @@
1769E32224BC5925000E1E8E /* AccountsPreferencesModel.swift in Sources */,
51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */,
51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */,
51C65AD924CC834F008EB3BD /* OrderedDictionary.swift in Sources */,
51E4991A24A8090F00B667CB /* IconImage.swift in Sources */,
1799E6AA24C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */,
51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */,
@ -5649,7 +5646,6 @@
65ED403F235DEF6C0081F399 /* ArticleRenderer.swift in Sources */,
65ED4040235DEF6C0081F399 /* GeneralPrefencesViewController.swift in Sources */,
179DB1DFBCF9177104B12E0F /* AccountsNewsBlurWindowController.swift in Sources */,
51C65AD624CC834F008EB3BD /* OrderedDictionary.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5807,7 +5803,6 @@
D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */,
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
51C65AD724CC834F008EB3BD /* OrderedDictionary.swift in Sources */,
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
@ -5939,7 +5934,6 @@
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */,
511B9806237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
84C9FC7722629E1200D921D6 /* AdvancedPreferencesViewController.swift in Sources */,
51C65AD524CC834F008EB3BD /* OrderedDictionary.swift in Sources */,
849EE72120391F560082A1EA /* SharingServicePickerDelegate.swift in Sources */,
5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */,
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,

View File

@ -1,64 +0,0 @@
//
// OrderedDictionary.swift
// SwiftDataStructures
//
// Created by Tim Ekl on 6/2/14.
// Copyright (c) 2014 Tim Ekl. Available under MIT License. See LICENSE.md.
//
import Foundation
struct OrderedDictionary<Tk: Hashable, Tv> {
var keys: Array<Tk> = []
var values: Dictionary<Tk,Tv> = [:]
var count: Int {
assert(keys.count == values.count, "Keys and values array out of sync")
return self.keys.count;
}
// Explicitly define an empty initializer to prevent the default memberwise initializer from being generated
init() {}
subscript(index: Int) -> Tv? {
get {
let key = self.keys[index]
return self.values[key]
}
set(newValue) {
let key = self.keys[index]
if (newValue != nil) {
self.values[key] = newValue
} else {
self.values[key] = nil
self.keys.remove(at: index)
}
}
}
subscript(key: Tk) -> Tv? {
get {
return self.values[key]
}
set(newValue) {
if newValue == nil {
self.values[key] = nil
self.keys = self.keys.filter {$0 != key}
} else {
let oldValue = self.values.updateValue(newValue!, forKey: key)
if oldValue == nil {
self.keys.append(key)
}
}
}
}
var description: String {
var result = "{\n"
for i in 0..<self.count {
result += "[\(i)]: \(self.keys[i]) => \(String(describing: self[i]))\n"
}
result += "}"
return result
}
}