From 2bf7f9a71c3d1b4b95aa31b5f5369b6d53287df0 Mon Sep 17 00:00:00 2001 From: David Sinclair Date: Mon, 4 Nov 2024 22:16:34 -0700 Subject: [PATCH] #1899 (dashboard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Populating the dashboard data. - Loading each folder/feed stories. - And more work in progress. - Switched back to Swift 5; Swift 6 isn’t ready for prime time yet (changing rapidly, several Apple APIs that don’t fully support it). --- clients/ios/Classes/DashList.swift | 32 ++++++++++++++++--- .../ios/Classes/FeedDetailDashListView.swift | 4 +-- .../Classes/FeedDetailObjCViewController.m | 4 +++ clients/ios/Classes/FeedsObjCViewController.m | 17 +++++++++- clients/ios/Classes/FeedsViewController.swift | 31 ++++++++++++++++++ clients/ios/Classes/StoryCache.swift | 32 ++++++++++++++++--- clients/ios/Classes/SwiftUtilities.swift | 19 +++++++++++ .../ios/NewsBlur.xcodeproj/project.pbxproj | 10 ++++-- 8 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 clients/ios/Classes/SwiftUtilities.swift diff --git a/clients/ios/Classes/DashList.swift b/clients/ios/Classes/DashList.swift index 6acc78e29c..b57a4452bd 100644 --- a/clients/ios/Classes/DashList.swift +++ b/clients/ios/Classes/DashList.swift @@ -11,12 +11,36 @@ import Foundation /// A list in the Dashboard. @MainActor class DashList: Identifiable { var index: Int - var feed: Feed + + enum Side: String { + case left + case right + } + + var side: Side + var order: Int + + var feedId: String? + var folder: String + + var feed: Feed? var stories = [Story]() - init(index: Int, feed: Feed, stories: [Story] = [Story]()) { + var isLoaded: Bool { + return feed != nil + } + + init(index: Int, side: Side, order: Int, feedId: String?, folder: String) { self.index = index - self.feed = feed - self.stories = stories + self.side = side + self.order = order + self.feedId = feedId + self.folder = folder + } +} + +extension DashList: @preconcurrency CustomStringConvertible { + var description: String { + return "DashList index: \(index), side: \(side), order: \(order), folder: \(folder), feed: \(feedId ?? "none"); \(feed != nil ? "\(feed?.name ?? "?"), stories: \(stories.count)" : "not loaded")" } } diff --git a/clients/ios/Classes/FeedDetailDashListView.swift b/clients/ios/Classes/FeedDetailDashListView.swift index be97f96795..4a9bad5216 100644 --- a/clients/ios/Classes/FeedDetailDashListView.swift +++ b/clients/ios/Classes/FeedDetailDashListView.swift @@ -39,14 +39,14 @@ struct DashListHeaderView: View { HStack { Spacer() - if let feedImage = dash.feed.image { + if let feedImage = dash.feed?.image { Image(uiImage: feedImage) .resizable() .frame(width: 16, height: 16) .padding(.leading, cache.settings.spacing == .compact ? 20 : 24) } - Text(dash.feed.name) + Text(dash.feed?.name ?? "Loading…") .lineLimit(1) .foregroundColor(Color.themed([0x404040, 0x404040, 0xC0C0C0, 0xB0B0B0])) diff --git a/clients/ios/Classes/FeedDetailObjCViewController.m b/clients/ios/Classes/FeedDetailObjCViewController.m index 3381ad8476..7874e429fc 100644 --- a/clients/ios/Classes/FeedDetailObjCViewController.m +++ b/clients/ios/Classes/FeedDetailObjCViewController.m @@ -1332,6 +1332,10 @@ - (void)finishedLoadingFeed:(NSDictionary *)results feedPage:(NSInteger)feedPage } #endif + if (self.dashboardIndex >= 0) { + [appDelegate.feedsViewController loadDashboard]; + } + self.pageFinished = NO; [self renderStories:confirmedNewStories]; diff --git a/clients/ios/Classes/FeedsObjCViewController.m b/clients/ios/Classes/FeedsObjCViewController.m index d463b36b85..24ccf1dcab 100644 --- a/clients/ios/Classes/FeedsObjCViewController.m +++ b/clients/ios/Classes/FeedsObjCViewController.m @@ -1028,6 +1028,14 @@ - (void)loadOfflineFeeds:(BOOL)failed { }]; } +- (void)clearDashboard { + @throw [NSException exceptionWithName:@"Missing clearDashboard implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil]; +} + +- (void)loadDashboard { + @throw [NSException exceptionWithName:@"Missing loadDashboard implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil]; +} + - (void)loadNotificationStory { @throw [NSException exceptionWithName:@"Missing loadNotificationStory implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil]; } @@ -1690,6 +1698,8 @@ - (void)tableView:(UITableView *)tableView [[tableView cellForRowAtIndexPath:indexPath] setNeedsDisplay]; + [self clearDashboard]; + if (searchFolder != nil) { [appDelegate loadRiverFeedDetailView:appDelegate.feedDetailViewController withFolder:searchFolder]; } else { @@ -1982,7 +1992,12 @@ - (void)didSelectSectionHeaderWithTag:(NSInteger)tag { folder = [NSString stringWithFormat:@"%ld", (long)tag]; } - [appDelegate loadRiverFeedDetailView:appDelegate.feedDetailViewController withFolder:folder]; + if ([folder isEqualToString:@"dashboard"]) { + [self loadDashboard]; + } else { + [self clearDashboard]; + [appDelegate loadRiverFeedDetailView:appDelegate.feedDetailViewController withFolder:folder]; + } if (!appDelegate.isPhone) { [appDelegate.feedDetailViewController viewWillAppear:NO]; diff --git a/clients/ios/Classes/FeedsViewController.swift b/clients/ios/Classes/FeedsViewController.swift index bee8c0c62b..b1f9374a0a 100644 --- a/clients/ios/Classes/FeedsViewController.swift +++ b/clients/ios/Classes/FeedsViewController.swift @@ -105,6 +105,37 @@ class FeedsViewController: FeedsObjCViewController { return parentTitles } + @objc func clearDashboard() { + appDelegate.feedDetailViewController.dashboardIndex = -1 + appDelegate.detailViewController.storyTitlesInDashboard = false + } + + @objc func loadDashboard() { + if appDelegate.feedDetailViewController.dashboardIndex >= 0 { + appDelegate.feedDetailViewController.storyCache.reloadDashboard(for: appDelegate.feedDetailViewController.dashboardIndex) + } + + appDelegate.feedDetailViewController.dashboardIndex += 1 + appDelegate.detailViewController.storyTitlesInDashboard = true + + let index = appDelegate.feedDetailViewController.dashboardIndex + + if index == 0 { + appDelegate.feedDetailViewController.storyCache.prepareDashboard() + } else if index >= appDelegate.dashboardArray.count { + // Done. + return + } + + let dash = appDelegate.feedDetailViewController.storyCache.dashboard[index] + + if let feed = dash.feedId { + appDelegate.loadFolder(dash.folder, feedID: feed) + } else { + appDelegate.loadRiverFeedDetailView(appDelegate.feedDetailViewController, withFolder: dash.folder) + } + } + var loadWorkItem: DispatchWorkItem? @objc func loadNotificationStory() { diff --git a/clients/ios/Classes/StoryCache.swift b/clients/ios/Classes/StoryCache.swift index 7231a69b4f..83cb96ebbe 100644 --- a/clients/ios/Classes/StoryCache.swift +++ b/clients/ios/Classes/StoryCache.swift @@ -135,19 +135,41 @@ import Foundation } } - func reloadDashboard(for index: Int) { - if index == 0 { - dashboard.removeAll() + func prepareDashboard() { + dashboard.removeAll() + + guard let dashboardArray = appDelegate.dashboardArray as? [[String : Any]] else { + return } + for (index, dashInfo) in dashboardArray.enumerated() { + guard let dashId = dashInfo["river_id"] as? String, + let order = dashInfo["river_order"] as? Int, + let sideString = dashInfo["river_side"] as? String, let side = DashList.Side(rawValue: sideString) else { + continue + } + + let feedId = dashId.hasPrefix("feed:") ? dashId.deletingPrefix("feed:") : nil + guard let folder = dashId == "river:" ? "everything" : dashId.hasPrefix("river:") ? dashId.deletingPrefix("river:") : appDelegate.parentFolders(forFeed: feedId).first as? String else { + continue + } + + let dash = DashList(index: index, side: side, order: order, feedId: feedId, folder: folder) + + dashboard.append(dash) + } + } + + func reloadDashboard(for index: Int) { reload() guard let currentFeed, index >= 0, index <= dashboard.count else { return } - let dash = DashList(index: index, feed: currentFeed, stories: before) + let dash = dashboard[index] - dashboard.append(dash) + dash.feed = currentFeed + dash.stories = before } } diff --git a/clients/ios/Classes/SwiftUtilities.swift b/clients/ios/Classes/SwiftUtilities.swift new file mode 100644 index 0000000000..d5ddd8419b --- /dev/null +++ b/clients/ios/Classes/SwiftUtilities.swift @@ -0,0 +1,19 @@ +// +// SwiftUtilities.swift +// NewsBlur +// +// Created by David Sinclair on 2024-11-04. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import Foundation + +extension String { + func deletingPrefix(_ prefix: String) -> String { + guard hasPrefix(prefix) else { + return self + } + + return String(dropFirst(prefix.count)) + } +} diff --git a/clients/ios/NewsBlur.xcodeproj/project.pbxproj b/clients/ios/NewsBlur.xcodeproj/project.pbxproj index 14d06424aa..007b5e5ac0 100755 --- a/clients/ios/NewsBlur.xcodeproj/project.pbxproj +++ b/clients/ios/NewsBlur.xcodeproj/project.pbxproj @@ -748,6 +748,8 @@ 176A5C7A24F8BD1B009E8DF9 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176A5C7924F8BD1B009E8DF9 /* DetailViewController.swift */; }; 176AC9EC2C797AA1007B8CB1 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176AC9EB2C797AA1007B8CB1 /* PreviewViewController.swift */; }; 176AC9ED2C797AA1007B8CB1 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176AC9EB2C797AA1007B8CB1 /* PreviewViewController.swift */; }; + 176B59662CD9C66A003B0C48 /* SwiftUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176B59652CD9C66A003B0C48 /* SwiftUtilities.swift */; }; + 176B59672CD9C66A003B0C48 /* SwiftUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176B59652CD9C66A003B0C48 /* SwiftUtilities.swift */; }; 17731A9D23DFAD3D00759A7D /* ImportExportPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17731A9C23DFAD3D00759A7D /* ImportExportPreferences.swift */; }; 177551D5238E228A00E27818 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 177551D4238E228A00E27818 /* NotificationCenter.framework */; platformFilter = ios; }; 177551DB238E228A00E27818 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 177551D9238E228A00E27818 /* MainInterface.storyboard */; }; @@ -1554,6 +1556,7 @@ 17654E452B02C08700F61B2B /* NewsBlur Alpha Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NewsBlur Alpha Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 176A5C7924F8BD1B009E8DF9 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 176AC9EB2C797AA1007B8CB1 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; + 176B59652CD9C66A003B0C48 /* SwiftUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUtilities.swift; sourceTree = ""; }; 17731A9C23DFAD3D00759A7D /* ImportExportPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportPreferences.swift; sourceTree = ""; }; 177551D3238E228A00E27818 /* Old NewsBlur Latest.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Old NewsBlur Latest.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 177551D4238E228A00E27818 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; @@ -3399,6 +3402,7 @@ FFE5322E144C8AC300ACFDE0 /* Utilities.m */, 43D818A115B940C200733444 /* DataUtilities.h */, 43D818A215B940C200733444 /* DataUtilities.m */, + 176B59652CD9C66A003B0C48 /* SwiftUtilities.swift */, 172ECBFF298B156D006371BC /* SwiftUIUtilities.swift */, 172AD273251D9F40000BB264 /* Storyboards.swift */, 170E3CCE24F8A5D8009CE819 /* SplitViewController.swift */, @@ -5175,6 +5179,7 @@ 175792AF2930605500490924 /* FMDatabaseQueue.m in Sources */, 175792B02930605500490924 /* FMResultSet.m in Sources */, 175792B12930605500490924 /* NBNotifier.m in Sources */, + 176B59662CD9C66A003B0C48 /* SwiftUtilities.swift in Sources */, 175792B22930605500490924 /* TUSafariActivity.m in Sources */, 17BC56AE2BBF6C0000A30C41 /* StoryCache.swift in Sources */, 175792B32930605500490924 /* PremiumViewController.m in Sources */, @@ -5388,6 +5393,7 @@ FF753CD1175858FC00344EC9 /* FMDatabaseQueue.m in Sources */, FF753CD3175858FC00344EC9 /* FMResultSet.m in Sources */, FF6618C8176184560039913B /* NBNotifier.m in Sources */, + 176B59672CD9C66A003B0C48 /* SwiftUtilities.swift in Sources */, FF03AFF319F87F2E0063002A /* TUSafariActivity.m in Sources */, 17BC56AD2BBF6C0000A30C41 /* StoryCache.swift in Sources */, FF83FF051FB52565008DAC0F /* PremiumViewController.m in Sources */, @@ -6129,7 +6135,7 @@ RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6184,7 +6190,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release;