2020-08-27 21:26:12 -07:00
|
|
|
//
|
|
|
|
// FeedDetailViewController.swift
|
|
|
|
// NewsBlur
|
|
|
|
//
|
|
|
|
// Created by David Sinclair on 2020-08-27.
|
|
|
|
// Copyright © 2020 NewsBlur. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
2023-01-21 16:20:53 -06:00
|
|
|
import SwiftUI
|
2020-08-27 21:26:12 -07:00
|
|
|
|
|
|
|
/// List of stories for a feed.
|
|
|
|
class FeedDetailViewController: FeedDetailObjCViewController {
|
2023-01-21 16:20:53 -06:00
|
|
|
lazy var gridViewController = makeGridViewController()
|
|
|
|
|
|
|
|
lazy var storyCache = StoryCache()
|
|
|
|
|
2022-09-02 20:39:00 -06:00
|
|
|
enum SectionLayoutKind: Int, CaseIterable {
|
|
|
|
/// Feed cells before the story.
|
|
|
|
case feedBeforeStory
|
|
|
|
|
|
|
|
/// The selected story.
|
|
|
|
case selectedStory
|
|
|
|
|
|
|
|
/// Feed cells after the story.
|
|
|
|
case feedAfterStory
|
|
|
|
|
|
|
|
/// Loading cell at the end.
|
|
|
|
case loading
|
|
|
|
}
|
2020-08-27 21:26:12 -07:00
|
|
|
|
2022-10-26 20:50:07 -06:00
|
|
|
var isGrid: Bool {
|
|
|
|
return appDelegate.detailViewController.layout == .grid
|
|
|
|
}
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
var wasGrid: Bool {
|
|
|
|
return appDelegate.detailViewController.wasGrid
|
|
|
|
}
|
|
|
|
|
|
|
|
var isExperimental: Bool {
|
|
|
|
return appDelegate.detailViewController.style == .experimental
|
|
|
|
}
|
|
|
|
|
|
|
|
var isSwiftUI: Bool {
|
|
|
|
return isGrid || isExperimental
|
|
|
|
}
|
|
|
|
|
2022-09-02 20:39:00 -06:00
|
|
|
var feedColumns: Int {
|
|
|
|
guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else {
|
|
|
|
return 4
|
|
|
|
}
|
|
|
|
|
|
|
|
return columns
|
|
|
|
}
|
|
|
|
|
2022-10-27 20:53:47 -06:00
|
|
|
var gridHeight: CGFloat {
|
|
|
|
guard let pref = UserDefaults.standard.string(forKey: "grid_height") else {
|
|
|
|
return 400
|
|
|
|
}
|
|
|
|
|
|
|
|
switch pref {
|
|
|
|
case "xs":
|
|
|
|
return 250
|
|
|
|
case "short":
|
|
|
|
return 300
|
|
|
|
case "tall":
|
|
|
|
return 400
|
|
|
|
case "xl":
|
|
|
|
return 450
|
|
|
|
default:
|
|
|
|
return 350
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-21 16:20:53 -06:00
|
|
|
private func makeGridViewController() -> UIHostingController<FeedDetailGridView> {
|
|
|
|
let gridView = FeedDetailGridView(feedDetailInteraction: self, cache: storyCache)
|
|
|
|
let gridViewController = UIHostingController(rootView: gridView)
|
|
|
|
gridViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
return gridViewController
|
|
|
|
}
|
|
|
|
|
2022-09-02 20:39:00 -06:00
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
2023-01-21 16:20:53 -06:00
|
|
|
|
|
|
|
addChild(gridViewController)
|
|
|
|
view.addSubview(gridViewController.view)
|
|
|
|
gridViewController.didMove(toParent: self)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
gridViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
gridViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
2023-04-21 21:44:28 -07:00
|
|
|
gridViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
gridViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
2023-01-21 16:20:53 -06:00
|
|
|
])
|
2023-05-25 21:47:34 -07:00
|
|
|
|
|
|
|
changedLayout()
|
2022-10-27 15:19:31 -06:00
|
|
|
}
|
|
|
|
|
2023-07-21 21:15:24 -06:00
|
|
|
@objc override func loadingFeed() {
|
|
|
|
// Make sure the view has loaded.
|
|
|
|
_ = view
|
|
|
|
|
|
|
|
if appDelegate.detailViewController.isPhone {
|
|
|
|
changedLayout()
|
2023-08-24 21:47:19 -06:00
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
|
|
|
|
self.reload()
|
|
|
|
}
|
2023-07-21 21:15:24 -06:00
|
|
|
} else {
|
2023-08-11 11:40:13 -06:00
|
|
|
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)
|
|
|
|
}
|
2023-07-21 21:15:24 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-27 15:19:31 -06:00
|
|
|
@objc override func changedLayout() {
|
2023-06-15 21:53:00 -07:00
|
|
|
// Make sure the view has loaded.
|
|
|
|
_ = view
|
|
|
|
|
2023-05-25 21:47:34 -07:00
|
|
|
storyTitlesTable.isHidden = !isLegacyTable
|
|
|
|
gridViewController.view.isHidden = isLegacyTable
|
|
|
|
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🪿 changedLayout for \(isLegacyTable ? "legacy table" : "SwiftUI grid layout")")
|
2023-08-11 11:40:13 -06:00
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
deferredReload()
|
|
|
|
}
|
|
|
|
|
|
|
|
var reloadWorkItem: DispatchWorkItem?
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
var pendingStories = [Story.ID : Story]()
|
|
|
|
|
2023-08-29 16:33:12 -06:00
|
|
|
var suppressMarkAsRead = false
|
|
|
|
|
2023-07-04 21:25:35 -07:00
|
|
|
func deferredReload(story: Story? = nil) {
|
2023-03-03 21:38:55 -07:00
|
|
|
reloadWorkItem?.cancel()
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
if let story {
|
|
|
|
pendingStories[story.id] = story
|
|
|
|
} else {
|
|
|
|
pendingStories.removeAll()
|
|
|
|
}
|
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
let workItem = DispatchWorkItem { [weak self] in
|
|
|
|
guard let self else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
if pendingStories.isEmpty {
|
|
|
|
configureDataSource()
|
|
|
|
} else {
|
|
|
|
for story in pendingStories.values {
|
|
|
|
configureDataSource(story: story)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pendingStories.removeAll()
|
2023-07-20 21:07:56 -06:00
|
|
|
reloadWorkItem = nil
|
2022-09-03 21:24:36 -06:00
|
|
|
}
|
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
reloadWorkItem = workItem
|
2023-03-15 17:03:34 -07:00
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: workItem)
|
2022-09-02 20:39:00 -06:00
|
|
|
}
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
@objc override func reloadImmediately() {
|
|
|
|
configureDataSource()
|
|
|
|
}
|
|
|
|
|
2022-09-02 20:39:00 -06:00
|
|
|
@objc override func reload() {
|
2023-03-15 17:03:34 -07:00
|
|
|
deferredReload()
|
2022-09-02 20:39:00 -06:00
|
|
|
}
|
2022-09-03 21:24:36 -06:00
|
|
|
|
2023-07-04 21:25:35 -07:00
|
|
|
func reload(story: Story) {
|
|
|
|
deferredReload(story: story)
|
|
|
|
}
|
2023-07-20 21:07:56 -06:00
|
|
|
|
|
|
|
@objc override func reload(_ indexPath: IndexPath, with rowAnimation: UITableView.RowAnimation = .none) {
|
|
|
|
if !isLegacyTable {
|
|
|
|
deferredReload()
|
|
|
|
} else if reloadWorkItem == nil {
|
|
|
|
// Only do this if a deferred reload isn't pending; otherwise no point in doing a partial reload, plus the table may be stale.
|
|
|
|
storyTitlesTable.reloadRows(at: [indexPath], with: rowAnimation)
|
|
|
|
}
|
|
|
|
}
|
2023-07-04 21:25:35 -07:00
|
|
|
}
|
2022-09-02 20:39:00 -06:00
|
|
|
|
|
|
|
extension FeedDetailViewController {
|
2023-07-04 21:25:35 -07:00
|
|
|
func configureDataSource(story: Story? = nil) {
|
|
|
|
if let story {
|
|
|
|
storyCache.reload(story: story)
|
|
|
|
} else {
|
|
|
|
storyCache.reload()
|
|
|
|
}
|
2022-09-02 20:39:00 -06:00
|
|
|
|
2023-05-25 21:47:34 -07:00
|
|
|
if isLegacyTable {
|
|
|
|
reloadTable()
|
|
|
|
}
|
2022-09-02 20:39:00 -06:00
|
|
|
}
|
2020-08-27 21:26:12 -07:00
|
|
|
}
|
2023-01-21 16:20:53 -06:00
|
|
|
|
|
|
|
extension FeedDetailViewController: FeedDetailInteraction {
|
2023-05-26 16:01:51 -07:00
|
|
|
var hasNoMoreStories: Bool {
|
|
|
|
return pageFinished
|
|
|
|
}
|
|
|
|
|
|
|
|
var isPremiumRestriction: Bool {
|
|
|
|
return !appDelegate.isPremium &&
|
|
|
|
storiesCollection.isRiverView &&
|
|
|
|
!storiesCollection.isReadView &&
|
|
|
|
!storiesCollection.isWidgetView &&
|
|
|
|
!storiesCollection.isSocialView &&
|
|
|
|
!storiesCollection.isSavedView
|
|
|
|
}
|
|
|
|
|
2023-04-21 21:44:28 -07:00
|
|
|
func pullToRefresh() {
|
|
|
|
instafetchFeed()
|
|
|
|
}
|
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
func visible(story: Story) {
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🐓 Visible: \(story.debugTitle)")
|
2023-01-21 16:20:53 -06:00
|
|
|
|
|
|
|
if story.index >= storyCache.before.count + storyCache.after.count - 5 {
|
|
|
|
if storiesCollection.isRiverView, storiesCollection.activeFolder != nil {
|
|
|
|
fetchRiverPage(storiesCollection.feedPage + 1, withCallback: nil)
|
|
|
|
} else {
|
|
|
|
fetchFeedDetail(storiesCollection.feedPage + 1, withCallback: nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
func tapped(story: Story) {
|
2023-05-26 14:00:20 -07:00
|
|
|
if presentedViewController != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🪿 Tapped \(story.debugTitle)")
|
2023-01-21 16:20:53 -06:00
|
|
|
|
|
|
|
let indexPath = IndexPath(row: story.index, section: 0)
|
|
|
|
|
2023-08-29 16:33:12 -06:00
|
|
|
suppressMarkAsRead = true
|
|
|
|
|
2023-03-31 22:02:58 -07:00
|
|
|
didSelectItem(at: indexPath)
|
2023-08-29 16:33:12 -06:00
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
|
|
self.suppressMarkAsRead = false
|
|
|
|
}
|
2023-01-21 16:20:53 -06:00
|
|
|
}
|
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
func reading(story: Story) {
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🪿 Reading \(story.debugTitle)")
|
2023-03-03 21:38:55 -07:00
|
|
|
}
|
|
|
|
|
2023-03-21 21:54:21 -07:00
|
|
|
func read(story: Story) {
|
2023-08-29 16:33:12 -06:00
|
|
|
if suppressMarkAsRead {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-21 21:54:21 -07:00
|
|
|
let dict = story.dictionary
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
if isSwiftUI, storiesCollection.isStoryUnread(dict) {
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🪿 Marking as read \(story.debugTitle)")
|
2023-03-21 21:54:21 -07:00
|
|
|
|
|
|
|
storiesCollection.markStoryRead(dict)
|
|
|
|
storiesCollection.syncStory(asRead: dict)
|
|
|
|
|
2023-07-04 21:25:35 -07:00
|
|
|
deferredReload(story: story)
|
2023-03-21 21:54:21 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-15 21:53:00 -07:00
|
|
|
func unread(story: Story) {
|
|
|
|
let dict = story.dictionary
|
|
|
|
|
2023-08-11 11:40:13 -06:00
|
|
|
if isSwiftUI, !storiesCollection.isStoryUnread(dict) {
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🪿 Marking as unread \(story.debugTitle)")
|
2023-06-15 21:53:00 -07:00
|
|
|
|
|
|
|
storiesCollection.markStoryUnread(dict)
|
|
|
|
storiesCollection.syncStory(asRead: dict)
|
|
|
|
|
2023-07-04 21:25:35 -07:00
|
|
|
deferredReload(story: story)
|
2023-06-15 21:53:00 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 21:38:55 -07:00
|
|
|
func hid(story: Story) {
|
2023-08-29 13:00:23 -06:00
|
|
|
print("🪿 Hiding \(story.debugTitle)")
|
2023-02-02 21:41:10 -06:00
|
|
|
|
|
|
|
appDelegate.activeStory = nil
|
|
|
|
reload()
|
|
|
|
}
|
2023-01-21 16:20:53 -06:00
|
|
|
}
|