From 75e4fb7e8548173f60a032df5b149fbbb89581db Mon Sep 17 00:00:00 2001 From: David Sinclair Date: Fri, 11 Aug 2023 11:40:13 -0600 Subject: [PATCH] #1720 (Grid view) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed list view stories incorrectly marked as read. - Fixed grid view wrong dates in story header. - Fixed grid view heading not matching story. - Fixed grid view showing the wrong story. - Fixed grid view duplicate stories. - Tapping grid view story heading now scrolls to show that story’s card. - Now always shows the Sites button in non-left layout, since the collapse fullscreen button doesn’t seem to work. - Fixed crash with malformed story. - Fixed loading feeds in different layouts. - And some other tweaks. --- .../ios/Classes/DetailViewController.swift | 12 ++--- clients/ios/Classes/FeedDetailGridView.swift | 10 ++-- .../Classes/FeedDetailObjCViewController.h | 1 + .../Classes/FeedDetailObjCViewController.m | 6 ++- .../Classes/FeedDetailViewController.swift | 50 +++++++++++++++++-- clients/ios/Classes/SplitViewDelegate.swift | 4 ++ clients/ios/Classes/StoriesCollection.m | 8 +++ clients/ios/Classes/Story.swift | 16 ++++-- .../ios/Resources/MainInterface.storyboard | 2 +- 9 files changed, 84 insertions(+), 25 deletions(-) diff --git a/clients/ios/Classes/DetailViewController.swift b/clients/ios/Classes/DetailViewController.swift index 0925f2cb7..8a92af4a5 100644 --- a/clients/ios/Classes/DetailViewController.swift +++ b/clients/ios/Classes/DetailViewController.swift @@ -251,7 +251,7 @@ class DetailViewController: BaseViewController { var feedDetailViewController: FeedDetailViewController? /// Whether or not the grid layout was used the last time checking the view controllers. - private var wasGrid = false + var wasGrid = false /// The horizontal page view controller. [Not currently used; might be used for #1351 (gestures in vertical scrolling).] // var horizontalPageViewController: HorizontalPageViewController? @@ -284,19 +284,13 @@ class DetailViewController: BaseViewController { } if layout != .left, let controller = feedDetailViewController { - if behavior == .overlay { - navigationItem.leftBarButtonItems = [controller.feedsBarButton, controller.settingsBarButton] - } else { - navigationItem.leftBarButtonItems = [controller.settingsBarButton] - } + navigationItem.leftBarButtonItems = [controller.feedsBarButton, controller.settingsBarButton] } else { navigationItem.leftBarButtonItems = [] } if reload { - DispatchQueue.main.async { - self.feedDetailViewController?.reload() - } + self.feedDetailViewController?.reload() } } diff --git a/clients/ios/Classes/FeedDetailGridView.swift b/clients/ios/Classes/FeedDetailGridView.swift index 87d4385f9..48596f3bd 100644 --- a/clients/ios/Classes/FeedDetailGridView.swift +++ b/clients/ios/Classes/FeedDetailGridView.swift @@ -103,14 +103,16 @@ struct FeedDetailGridView: View { } } .onChange(of: cache.selected) { [oldSelected = cache.selected] newSelected in - guard let newSelected, oldSelected?.hash != newSelected.hash else { + guard oldSelected?.hash != newSelected?.hash else { return } - print("\(oldSelected?.title ?? "none") -> \(newSelected.title)") + print("\(oldSelected?.title ?? "none") -> \(newSelected?.title ?? "none")") Task { - if !cache.isGrid { + if newSelected == nil, !cache.isPhone, let oldSelected, let story = cache.story(with: oldSelected.index) { + scroller.scrollTo(story.id, anchor: .top) + } else if let newSelected, !cache.isGrid { withAnimation(Animation.spring().delay(0.5)) { scroller.scrollTo(newSelected.id) } @@ -118,7 +120,7 @@ struct FeedDetailGridView: View { withAnimation(Animation.spring().delay(0.5)) { scroller.scrollTo(storyViewID, anchor: .top) } - } else { + } else if let newSelected { scroller.scrollTo(newSelected.id, anchor: .top) } } diff --git a/clients/ios/Classes/FeedDetailObjCViewController.h b/clients/ios/Classes/FeedDetailObjCViewController.h index c2b8a4212..8434d2994 100644 --- a/clients/ios/Classes/FeedDetailObjCViewController.h +++ b/clients/ios/Classes/FeedDetailObjCViewController.h @@ -75,6 +75,7 @@ - (void)loadingFeed; - (void)changedLayout; - (void)reload; +- (void)reloadImmediately; - (void)reloadTable; - (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation; - (void)reloadWithSizing; diff --git a/clients/ios/Classes/FeedDetailObjCViewController.m b/clients/ios/Classes/FeedDetailObjCViewController.m index 1660a8a19..539e30ef9 100644 --- a/clients/ios/Classes/FeedDetailObjCViewController.m +++ b/clients/ios/Classes/FeedDetailObjCViewController.m @@ -344,6 +344,10 @@ typedef NS_ENUM(NSUInteger, FeedSection) @throw [NSException exceptionWithName:@"Missing reload implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil]; } +- (void)reloadImmediately { + @throw [NSException exceptionWithName:@"Missing reloadImmediately implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil]; +} + - (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation { @throw [NSException exceptionWithName:@"Missing reloadIndexPath implementation" reason:@"This is implemented in the Swift subclass, so should never reach here." userInfo:nil]; } @@ -707,7 +711,7 @@ typedef NS_ENUM(NSUInteger, FeedSection) [self.notifier hideIn:0]; [self beginOfflineTimer]; [appDelegate.cacheImagesOperationQueue cancelAllOperations]; - [self reload]; +// [self reload]; } - (void)reloadStories { diff --git a/clients/ios/Classes/FeedDetailViewController.swift b/clients/ios/Classes/FeedDetailViewController.swift index 9b225e4fa..a7b1a24da 100644 --- a/clients/ios/Classes/FeedDetailViewController.swift +++ b/clients/ios/Classes/FeedDetailViewController.swift @@ -33,6 +33,18 @@ class FeedDetailViewController: FeedDetailObjCViewController { return appDelegate.detailViewController.layout == .grid } + var wasGrid: Bool { + return appDelegate.detailViewController.wasGrid + } + + var isExperimental: Bool { + return appDelegate.detailViewController.style == .experimental + } + + var isSwiftUI: Bool { + return isGrid || isExperimental + } + var feedColumns: Int { guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else { return 4 @@ -92,8 +104,14 @@ class FeedDetailViewController: FeedDetailObjCViewController { if appDelegate.detailViewController.isPhone { changedLayout() } else { - DispatchQueue.main.async { - self.appDelegate.detailViewController.updateLayout(reload: true, fetchFeeds: false) + let wasGrid = wasGrid + + self.appDelegate.detailViewController.updateLayout(reload: false, fetchFeeds: false) + + if wasGrid != isGrid { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { + self.appDelegate.detailViewController.updateLayout(reload: true, fetchFeeds: false) + } } } } @@ -105,20 +123,38 @@ class FeedDetailViewController: FeedDetailObjCViewController { storyTitlesTable.isHidden = !isLegacyTable gridViewController.view.isHidden = isLegacyTable + print("changedLayout for \(isLegacyTable ? "legacy table" : "SwiftUI grid layout")") + deferredReload() } var reloadWorkItem: DispatchWorkItem? + var pendingStories = [Story.ID : Story]() + func deferredReload(story: Story? = nil) { reloadWorkItem?.cancel() + if let story { + pendingStories[story.id] = story + } else { + pendingStories.removeAll() + } + let workItem = DispatchWorkItem { [weak self] in guard let self else { return } - configureDataSource(story: story) + if pendingStories.isEmpty { + configureDataSource() + } else { + for story in pendingStories.values { + configureDataSource(story: story) + } + } + + pendingStories.removeAll() reloadWorkItem = nil } @@ -126,6 +162,10 @@ class FeedDetailViewController: FeedDetailObjCViewController { DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: workItem) } + @objc override func reloadImmediately() { + configureDataSource() + } + @objc override func reload() { deferredReload() } @@ -207,7 +247,7 @@ extension FeedDetailViewController: FeedDetailInteraction { func read(story: Story) { let dict = story.dictionary - if storiesCollection.isStoryUnread(dict) { + if isSwiftUI, storiesCollection.isStoryUnread(dict) { print("marking as read '\(story.title)'") storiesCollection.markStoryRead(dict) @@ -222,7 +262,7 @@ extension FeedDetailViewController: FeedDetailInteraction { func unread(story: Story) { let dict = story.dictionary - if !storiesCollection.isStoryUnread(dict) { + if isSwiftUI, !storiesCollection.isStoryUnread(dict) { print("marking as unread '\(story.title)'") storiesCollection.markStoryUnread(dict) diff --git a/clients/ios/Classes/SplitViewDelegate.swift b/clients/ios/Classes/SplitViewDelegate.swift index 2569c4811..7bdb66b68 100644 --- a/clients/ios/Classes/SplitViewDelegate.swift +++ b/clients/ios/Classes/SplitViewDelegate.swift @@ -42,4 +42,8 @@ class SplitViewDelegate: NSObject, UISplitViewControllerDelegate { return proposedDisplayMode } } + + func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) { + + } } diff --git a/clients/ios/Classes/StoriesCollection.m b/clients/ios/Classes/StoriesCollection.m index 157a12fc1..28127e746 100644 --- a/clients/ios/Classes/StoriesCollection.m +++ b/clients/ios/Classes/StoriesCollection.m @@ -374,6 +374,10 @@ #pragma mark - Story Management - (void)addStories:(NSArray *)stories { + if (self.activeFeedStories == nil) { + NSLog(@"addStories: activeFeedStories was nil!"); + self.activeFeedStories = [NSMutableArray array]; + } self.activeFeedStories = [self.activeFeedStories arrayByAddingObjectsFromArray:stories]; self.storyCount = (int)[self.activeFeedStories count]; [self calculateStoryLocations]; @@ -424,6 +428,10 @@ NSString *hash = story[@"story_hash"]; NSString *title = story[@"story_title"]; + if (!hash) { + NSLog(@"🔧 trying to sync as read with no hash: %@: %@", hash, title); // log + return; + } if (self.recentlyReadHashes[hash]) { NSLog(@"🔧 trying to sync as read when already read: %@: %@", hash, title); // log return; diff --git a/clients/ios/Classes/Story.swift b/clients/ios/Classes/Story.swift index 041be8c49..99e5f6f63 100644 --- a/clients/ios/Classes/Story.swift +++ b/clients/ios/Classes/Story.swift @@ -32,9 +32,7 @@ class Story: Identifiable { var author = "" var dateAndAuthor: String { - let date = Utilities.formatShortDate(fromTimestamp: timestamp) ?? "" - - return author.isEmpty ? date : "\(date) · \(author)" + return author.isEmpty ? dateString : "\(dateString) · \(author)" } var isRiverOrSocial = true @@ -90,8 +88,8 @@ class Story: Identifiable { title = (string(for: "story_title") as NSString).decodingHTMLEntities() content = String(string(for: "story_content").convertHTML().decodingXMLEntities().decodingHTMLEntities().replacingOccurrences(of: "\n", with: " ").prefix(500)) author = string(for: "story_authors").replacingOccurrences(of: "\"", with: "") - dateString = string(for: "short_parsed_date") timestamp = dictionary["story_timestamp"] as? Int ?? 0 + dateString = Utilities.formatShortDate(fromTimestamp: timestamp) ?? "" isSaved = dictionary["starred"] as? Bool ?? false isShared = dictionary["shared"] as? Bool ?? false hash = string(for: "story_hash") @@ -124,7 +122,11 @@ extension Story: Equatable { extension Story: CustomDebugStringConvertible { var debugDescription: String { - return "Story \"\(title)\" in \(feedName)" + if isLoaded { + return "Story #\(index) \"\(title)\" in \(feedName)" + } else { + return "Story #\(index) (not loaded)" + } } } @@ -162,6 +164,10 @@ class StoryCache: ObservableObject { } } + func story(with index: Int) -> Story? { + return all.first(where: { $0.index == index } ) + } + func reload() { let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount) var beforeSelection = [Int]() diff --git a/clients/ios/Resources/MainInterface.storyboard b/clients/ios/Resources/MainInterface.storyboard index 3cbda2bd1..2df93ac75 100644 --- a/clients/ios/Resources/MainInterface.storyboard +++ b/clients/ios/Resources/MainInterface.storyboard @@ -14,7 +14,7 @@ - +