diff --git a/Multiplatform/Shared/Add/AddWebFeedView.swift b/Multiplatform/Shared/Add/AddWebFeedView.swift index 7c8f1615a..fd3e62d09 100644 --- a/Multiplatform/Shared/Add/AddWebFeedView.swift +++ b/Multiplatform/Shared/Add/AddWebFeedView.swift @@ -129,25 +129,25 @@ struct AddWebFeedView: View { @ViewBuilder var folderPicker: some View { #if os(iOS) Picker("Folder", selection: $viewModel.selectedFolderIndex, content: { - ForEach(0.. 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) + } + +} diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index 135d80c2f..7bbae55c9 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -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, Never>? + var timelineItemsPublisher: AnyPublisher? var articlesPublisher: AnyPublisher<[Article], Never>? var selectedTimelineItemsPublisher: AnyPublisher<[TimelineItem], Never>? var selectedArticlesPublisher: AnyPublisher<[Article], Never>? - + var articleStatusChangePublisher: AnyPublisher, Never>? + var readFilterEnabledTable = [FeedIdentifier: Bool]() var undoManager: UndoManager? @@ -45,7 +46,7 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { private var sortDirectionSubject = ReplaySubject(bufferSize: 1) private var groupByFeedSubject = ReplaySubject(bufferSize: 1) - private var timelineItems = OrderedDictionary() + 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 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 } + .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() + 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 { - var items = OrderedDictionary() - 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 } diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index cd8c49d5b..6be7b6f4a 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -11,7 +11,7 @@ import SwiftUI struct TimelineView: View { @EnvironmentObject private var timelineModel: TimelineModel - @State private var timelineItems = OrderedDictionary() + @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 diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index acec1923f..7f374987a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; - 51C65AD424CC834F008EB3BD /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; + 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItems.swift; sourceTree = ""; }; 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Shared/Extensions/OrderedDictionary.swift b/Shared/Extensions/OrderedDictionary.swift deleted file mode 100644 index 46e260d15..000000000 --- a/Shared/Extensions/OrderedDictionary.swift +++ /dev/null @@ -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 { - var keys: Array = [] - var values: Dictionary = [:] - - 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..