From a383b7b7904733f7dfb2169ed9c7dfad0c88dd2c Mon Sep 17 00:00:00 2001 From: David Sinclair Date: Thu, 23 Jan 2025 21:47:04 -0500 Subject: [PATCH] #1899 (dashboard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The folder and feed names and icons are now populated when first showing the Dashboard, before loading the stories. - The Dashboard is automatically refreshed every five minutes, while it is displayed. - Tapping a Dashboard list header goes to that folder or feed. - (Tapping a story isn’t implemented yet.) --- clients/ios/Classes/DashList.swift | 16 +++- .../ios/Classes/FeedDetailDashListView.swift | 4 +- clients/ios/Classes/FeedDetailGridView.swift | 1 + .../Classes/FeedDetailObjCViewController.m | 4 + .../Classes/FeedDetailViewController.swift | 21 ++++++ clients/ios/Classes/FeedsObjCViewController.h | 8 +- clients/ios/Classes/FeedsObjCViewController.m | 74 ++++++++++++++++--- clients/ios/Classes/FeedsViewController.swift | 29 ++++++-- 8 files changed, 133 insertions(+), 24 deletions(-) diff --git a/clients/ios/Classes/DashList.swift b/clients/ios/Classes/DashList.swift index cd9c8f4c59..26d5be9802 100644 --- a/clients/ios/Classes/DashList.swift +++ b/clients/ios/Classes/DashList.swift @@ -28,8 +28,8 @@ import Foundation var feeds = [Feed]() var stories = [Story]() - var isLoaded: Bool { - return folder != nil + var hasStories: Bool { + return !stories.isEmpty } var isFolder: Bool { @@ -62,6 +62,16 @@ import Foundation self.order = order self.feedId = feedId self.folderId = folderId + + load() + } + + func load() { + if isFolder { + folder = Folder(id: folderId) + } else if let feedId { + feeds = [Feed(id: feedId)] + } } } @@ -69,7 +79,7 @@ extension DashList: @preconcurrency CustomStringConvertible { var description: String { let base = "DashList index: \(index), side: \(side), order: \(order)" - if isLoaded { + if hasStories { if isFolder { return "\(base), folder: `\(folder?.name ?? "none")` (\(folderId)) contains \(feeds.count) feeds with \(stories.count) stories" } else { diff --git a/clients/ios/Classes/FeedDetailDashListView.swift b/clients/ios/Classes/FeedDetailDashListView.swift index f5bb0e7545..8a8d89e62e 100644 --- a/clients/ios/Classes/FeedDetailDashListView.swift +++ b/clients/ios/Classes/FeedDetailDashListView.swift @@ -58,7 +58,7 @@ struct DashListHeaderView: View { .frame(height: 50) .clipShape(RoundedRectangle(cornerRadius: 10)) .onTapGesture { -// interaction.hid(story: story) + interaction.tapped(dash: dash) } } } @@ -72,7 +72,7 @@ struct DashListStoriesView: View { var body: some View { VStack(alignment: .center) { - if dash.isLoaded { + if dash.hasStories { ForEach(dash.stories) { story in CardView(feedDetailInteraction: interaction, cache: cache, story: story) } diff --git a/clients/ios/Classes/FeedDetailGridView.swift b/clients/ios/Classes/FeedDetailGridView.swift index 34580817e6..9b9d1b895c 100644 --- a/clients/ios/Classes/FeedDetailGridView.swift +++ b/clients/ios/Classes/FeedDetailGridView.swift @@ -15,6 +15,7 @@ import SwiftUI var isMarkReadOnScroll: Bool { get } func pullToRefresh() + func tapped(dash: DashList) func visible(story: Story) func tapped(story: Story) func reading(story: Story) diff --git a/clients/ios/Classes/FeedDetailObjCViewController.m b/clients/ios/Classes/FeedDetailObjCViewController.m index 2662d98376..c4ffc04f0d 100644 --- a/clients/ios/Classes/FeedDetailObjCViewController.m +++ b/clients/ios/Classes/FeedDetailObjCViewController.m @@ -1230,6 +1230,10 @@ - (void)fetchRiverPage:(int)page withCallback:(void(^)(void))callback { #pragma mark Processing Stories - (void)finishedLoadingFeed:(NSDictionary *)results feedPage:(NSInteger)feedPage feedId:(NSString *)sentFeedId { + if (self.dashboardIndex >= 0 && !self.isDashboard) { + return; + } + appDelegate.hasLoadedFeedDetail = YES; self.isOnline = YES; self.isShowingFetching = NO; diff --git a/clients/ios/Classes/FeedDetailViewController.swift b/clients/ios/Classes/FeedDetailViewController.swift index 7c9fabf66f..231eb05d92 100644 --- a/clients/ios/Classes/FeedDetailViewController.swift +++ b/clients/ios/Classes/FeedDetailViewController.swift @@ -334,6 +334,14 @@ extension FeedDetailViewController: FeedDetailInteraction { instafetchFeed() } + func tapped(dash: DashList) { + if dash.isFolder { + appDelegate.feedsViewController.selectFolder(dash.folderId) + } else if let feedId = dash.feedId { + appDelegate.feedsViewController.selectFeed(feedId, inFolder: dash.folderId) + } + } + func visible(story: Story) { print("🐓 Visible: \(story.debugTitle)") @@ -365,6 +373,11 @@ extension FeedDetailViewController: FeedDetailInteraction { print("🪿 Tapped \(story.debugTitle)") + if isDashboard { + tappedDashboard(story: story) + return + } + let indexPath = IndexPath(row: story.index, section: 0) suppressMarkAsRead = true @@ -376,6 +389,14 @@ extension FeedDetailViewController: FeedDetailInteraction { } } + func tappedDashboard(story: Story) { +// guard let feedId = story.feed?.id else { +// return +// } + + //TODO: 🚧 + } + func reading(story: Story) { print("🪿 Reading \(story.debugTitle)") } diff --git a/clients/ios/Classes/FeedsObjCViewController.h b/clients/ios/Classes/FeedsObjCViewController.h index 49bba81f8d..de275a9470 100644 --- a/clients/ios/Classes/FeedsObjCViewController.h +++ b/clients/ios/Classes/FeedsObjCViewController.h @@ -110,6 +110,12 @@ UIGestureRecognizerDelegate, UISearchBarDelegate> { - (void)selectNextFolder:(id)sender; - (void)selectPreviousFolder:(id)sender; +- (void)selectWidgetStories; + +- (void)selectFolder:(NSString *)folder; +- (void)selectFeed:(NSString *)feedId inFolder:(NSString *)folder; + + - (void)markFeedRead:(NSString *)feedId cutoffDays:(NSInteger)days; - (void)markFeedsRead:(NSArray *)feedIds cutoffDays:(NSInteger)days; - (void)markEverythingReadWithDays:(NSInteger)days; @@ -136,8 +142,6 @@ UIGestureRecognizerDelegate, UISearchBarDelegate> { - (void)fadeFeed:(NSString *)feedId; - (IBAction)tapAddSite:(id)sender; -- (void)selectWidgetStories; - - (void)reloadFeedTitlesTable; - (void)resetToolbar; - (void)layoutHeaderCounts:(UIInterfaceOrientation)orientation; diff --git a/clients/ios/Classes/FeedsObjCViewController.m b/clients/ios/Classes/FeedsObjCViewController.m index 2c69ccbd43..0a518f52b9 100644 --- a/clients/ios/Classes/FeedsObjCViewController.m +++ b/clients/ios/Classes/FeedsObjCViewController.m @@ -2147,12 +2147,7 @@ - (void)selectNextFolder:(id)sender { } while (!foundNext && section != stopAtSection); [self didSelectSectionHeaderWithTag:section]; - - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:section]; - - if ([self.feedTitlesTable numberOfRowsInSection:section] > 0) { - [self.feedTitlesTable scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; - } + [self scrollToSection:section]; } - (void)selectPreviousFolder:(id)sender { @@ -2165,12 +2160,7 @@ - (void)selectPreviousFolder:(id)sender { } [self didSelectSectionHeaderWithTag:section]; - - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:section]; - - if ([self.feedTitlesTable numberOfRowsInSection:section] > 0) { - [self.feedTitlesTable scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; - } + [self scrollToSection:section]; } - (void)selectDashboard:(id)sender { @@ -2189,6 +2179,43 @@ - (void)selectWidgetStories { } } +- (void)selectFolder:(NSString *)folder { + NSInteger tag = [appDelegate.dictFoldersArray indexOfObject:folder]; + + if (tag != NSNotFound) { + [self didSelectSectionHeaderWithTag:tag]; + [self scrollToSection:tag]; + } +} + +- (void)selectFeed:(NSString *)feedId inFolder:(NSString *)folder { + NSInteger section = [appDelegate.dictFoldersArray indexOfObject:folder]; + NSArray *feedsInFolder = [appDelegate.dictFolders objectForKey:folder]; + + if (section != NSNotFound) { + for (NSInteger row = 0; row < feedsInFolder.count; row++) { + id thisFeedId = [feedsInFolder objectAtIndex:row]; + NSString *thisFeedIdStr = [NSString stringWithFormat:@"%@", thisFeedId]; + + if ([thisFeedIdStr isEqualToString:feedId]) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + + [self expandFolderIfNecessary:folder]; + [feedTitlesTable selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle]; + [self tableView:feedTitlesTable didSelectRowAtIndexPath:indexPath]; + } + } + } +} + +- (void)scrollToSection:(NSInteger)section { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:section]; + + if ([self.feedTitlesTable numberOfRowsInSection:section] > 0) { + [self.feedTitlesTable scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; + } +} + #pragma mark - MCSwipeTableViewCellDelegate // When the user starts swiping the cell this method is called @@ -2406,6 +2433,29 @@ - (void)didCollapseFolder:(UIButton *)button { } +- (void)expandFolderIfNecessary:(NSString *)folderName { + NSUserDefaults *userPreferences = [NSUserDefaults standardUserDefaults]; + + NSString *collapseKey = [NSString stringWithFormat:@"folderCollapsed:%@", folderName]; + bool isFolderCollapsed = [userPreferences boolForKey:collapseKey]; + + if (isFolderCollapsed) { + // Expand folder + [userPreferences setBool:NO forKey:collapseKey]; + [userPreferences synchronize]; + appDelegate.collapsedFolders = nil; + + [self resetRowHeights]; + + NSInteger tag = [appDelegate.dictFoldersArray indexOfObject:folderName]; + + [self.feedTitlesTable beginUpdates]; + [self.feedTitlesTable reloadSections:[NSIndexSet indexSetWithIndex:tag] + withRowAnimation:UITableViewRowAnimationFade]; + [self.feedTitlesTable endUpdates]; + } +} + - (BOOL)isFeedVisible:(id)feedId { if (![feedId isKindOfClass:[NSString class]]) { feedId = [NSString stringWithFormat:@"%@",feedId]; diff --git a/clients/ios/Classes/FeedsViewController.swift b/clients/ios/Classes/FeedsViewController.swift index c78d194afb..ee102985ce 100644 --- a/clients/ios/Classes/FeedsViewController.swift +++ b/clients/ios/Classes/FeedsViewController.swift @@ -105,9 +105,20 @@ class FeedsViewController: FeedsObjCViewController { return parentTitles } + var dashboardTimer: Timer? + @objc func clearDashboard() { appDelegate.feedDetailViewController.dashboardIndex = -1 appDelegate.detailViewController.storyTitlesInDashboard = false + + dashboardTimer?.invalidate() + dashboardTimer = nil + } + + @objc func reloadDashboard() { + appDelegate.feedDetailViewController.dashboardIndex = -1 + + immediatelyLoadNextDash(prepare: false) } @objc func loadDashboard() { @@ -116,7 +127,13 @@ class FeedsViewController: FeedsObjCViewController { } else if appDelegate.feedDetailViewController.dashboardIndex >= 0 { deferredLoadNextDash() } else { - immediatelyLoadNextDash() + let frequency: TimeInterval = 5 * 60 + + dashboardTimer?.invalidate() + dashboardTimer = Timer.scheduledTimer(timeInterval: frequency, target: self, selector: #selector(reloadDashboard), userInfo: nil, repeats: true + ) + + immediatelyLoadNextDash(prepare: true) } } @@ -126,18 +143,18 @@ class FeedsViewController: FeedsObjCViewController { dashWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in - guard let self else { + guard let self, isDashboard else { return } - immediatelyLoadNextDash() + immediatelyLoadNextDash(prepare: true) } dashWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: workItem) } - private func immediatelyLoadNextDash() { + private func immediatelyLoadNextDash(prepare: Bool) { appDelegate.feedDetailViewController.storyCache.reloadDashboard(for: appDelegate.feedDetailViewController.dashboardIndex) appDelegate.feedDetailViewController.dashboardIndex += 1 @@ -145,7 +162,9 @@ class FeedsViewController: FeedsObjCViewController { let index = appDelegate.feedDetailViewController.dashboardIndex if index == 0 { - appDelegate.feedDetailViewController.storyCache.prepareDashboard() + if prepare { + appDelegate.feedDetailViewController.storyCache.prepareDashboard() + } } else if index >= appDelegate.dashboardArray.count { // Done.