- 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.
This commit is contained in:
David Sinclair 2023-08-11 11:40:13 -06:00
parent 8687a6c7c5
commit 75e4fb7e85
9 changed files with 84 additions and 25 deletions

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -75,6 +75,7 @@
- (void)loadingFeed;
- (void)changedLayout;
- (void)reload;
- (void)reloadImmediately;
- (void)reloadTable;
- (void)reloadIndexPath:(NSIndexPath *)indexPath withRowAnimation:(UITableViewRowAnimation)rowAnimation;
- (void)reloadWithSizing;

View file

@ -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 {

View file

@ -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)

View file

@ -42,4 +42,8 @@ class SplitViewDelegate: NSObject, UISplitViewControllerDelegate {
return proposedDisplayMode
}
}
func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
}
}

View file

@ -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;

View file

@ -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]()

View file

@ -14,7 +14,7 @@
<objects>
<viewController storyboardIdentifier="DetailViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="djW-7k-haK" customClass="DetailViewController" customModule="NewsBlur" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jTZ-4O-xyT">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<rect key="frame" x="0.0" y="0.0" width="818.5" height="834"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bPa-u1-Aml">
<rect key="frame" x="0.0" y="74" width="1194" height="580"/>