Extract TimelineLikeDataSource into separate protocol
This commit is contained in:
parent
c740fb1c1f
commit
99a58e2c33
|
@ -40,7 +40,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.controller = TimelineLikeController(delegate: self, ownerType: String(describing: self))
|
||||
self.controller = TimelineLikeController(delegate: self, dataSource: self, ownerType: String(describing: self))
|
||||
|
||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Notifications"))
|
||||
}
|
||||
|
@ -387,8 +387,16 @@ extension NotificationsCollectionViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: TimelineLikeControllerDelegate
|
||||
// MARK: TimelineLikeCollectionViewController
|
||||
extension NotificationsCollectionViewController {
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationsCollectionViewController: TimelineLikeControllerDataSource {
|
||||
typealias TimelineItem = NotificationGroup
|
||||
|
||||
private static let pageSize = 40
|
||||
|
@ -478,27 +486,6 @@ extension NotificationsCollectionViewController {
|
|||
return NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes)
|
||||
}
|
||||
|
||||
func handlePrependItems(_ timelineItems: [NotificationGroup]) async {
|
||||
let topItem = dataSource.snapshot().itemIdentifiers(inSection: .notifications).first
|
||||
|
||||
// we always replace all, because new items are merged with existing ones
|
||||
await handleReplaceAllItems(timelineItems)
|
||||
|
||||
// preserve the scroll position
|
||||
// todo: this won't work for cmd+r when not at top
|
||||
if let topID = topItem?.group?.notifications.first?.id {
|
||||
// the exact item may have changed, due to merging
|
||||
let newTopGroup = timelineItems.first {
|
||||
$0.notifications.contains {
|
||||
$0.id == topID
|
||||
}
|
||||
}!
|
||||
if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup, nil, nil)) {
|
||||
collectionView.scrollToItem(at: newTopIndexPath, at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadOlder() async throws -> [NotificationGroup] {
|
||||
guard let older else {
|
||||
throw Error.noOlder
|
||||
|
@ -523,16 +510,34 @@ extension NotificationsCollectionViewController {
|
|||
let existingGroups = dataSource.snapshot().itemIdentifiers(inSection: .notifications).compactMap(\.group)
|
||||
return NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TimelineLikeControllerDelegate
|
||||
extension NotificationsCollectionViewController {
|
||||
func handlePrependItems(_ timelineItems: [NotificationGroup]) async {
|
||||
let topItem = dataSource.snapshot().itemIdentifiers(inSection: .notifications).first
|
||||
|
||||
// we always replace all, because new items are merged with existing ones
|
||||
await handleReplaceAllItems(timelineItems)
|
||||
|
||||
// preserve the scroll position
|
||||
// todo: this won't work for cmd+r when not at top
|
||||
if let topID = topItem?.group?.notifications.first?.id {
|
||||
// the exact item may have changed, due to merging
|
||||
let newTopGroup = timelineItems.first {
|
||||
$0.notifications.contains {
|
||||
$0.id == topID
|
||||
}
|
||||
}!
|
||||
if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup, nil, nil)) {
|
||||
collectionView.scrollToItem(at: newTopIndexPath, at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAppendItems(_ timelineItems: [NotificationGroup]) async {
|
||||
await handleReplaceAllItems(timelineItems)
|
||||
}
|
||||
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
||||
|
|
|
@ -45,7 +45,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.controller = TimelineLikeController(delegate: self, ownerType: String(describing: self))
|
||||
self.controller = TimelineLikeController(delegate: self, dataSource: self, ownerType: String(describing: self))
|
||||
|
||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Profile"))
|
||||
}
|
||||
|
@ -501,7 +501,16 @@ extension ProfileStatusesViewController {
|
|||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: TimelineLikeControllerDelegate {
|
||||
// MARK: TimelineLikeCollectionViewController
|
||||
extension ProfileStatusesViewController {
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: TimelineLikeControllerDataSource {
|
||||
typealias TimelineItem = String // status ID
|
||||
|
||||
private func request(for range: RequestRange = .default) -> Request<[Status]> {
|
||||
|
@ -572,12 +581,6 @@ extension ProfileStatusesViewController: TimelineLikeControllerDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: UICollectionViewDelegate {
|
||||
|
|
|
@ -66,7 +66,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.controller = TimelineLikeController(delegate: self, ownerType: String(describing: self))
|
||||
self.controller = TimelineLikeController(delegate: self, dataSource: self, ownerType: String(describing: self))
|
||||
|
||||
self.navigationItem.title = timeline.title
|
||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Timeline"))
|
||||
|
@ -1092,8 +1092,18 @@ extension TimelineViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: TimelineLikeControllerDelegate
|
||||
// MARK: TimelineLikeCollectionViewController
|
||||
extension TimelineViewController {
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noClient
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
case noGap
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: TimelineLikeControllerDataSource {
|
||||
typealias TimelineItem = String // status ID
|
||||
|
||||
// the maximum mastodon will provide in a single request
|
||||
|
@ -1209,7 +1219,10 @@ extension TimelineViewController {
|
|||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TimelineLikeControllerDelegate
|
||||
extension TimelineViewController {
|
||||
func handleFillGap(_ timelineItems: [String], direction: TimelineGapDirection) async {
|
||||
var snapshot = dataSource.snapshot()
|
||||
let addedItems: Bool
|
||||
|
@ -1297,14 +1310,6 @@ extension TimelineViewController {
|
|||
showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
enum Error: TimelineLikeCollectionViewError {
|
||||
case noClient
|
||||
case noNewer
|
||||
case noOlder
|
||||
case allCaughtUp
|
||||
case noGap
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineViewController: UICollectionViewDelegate {
|
||||
|
|
|
@ -10,19 +10,19 @@ import Foundation
|
|||
import OSLog
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
|
||||
protocol TimelineLikeControllerDataSource<TimelineItem>: AnyObject {
|
||||
associatedtype TimelineItem: Sendable
|
||||
|
||||
func loadInitial() async throws -> [TimelineItem]
|
||||
|
||||
func loadNewer() async throws -> [TimelineItem]
|
||||
|
||||
func canLoadOlder() async -> Bool
|
||||
|
||||
func loadOlder() async throws -> [TimelineItem]
|
||||
|
||||
func loadGap(in direction: TimelineGapDirection) async throws -> [TimelineItem]
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
|
||||
associatedtype TimelineItem: Sendable
|
||||
|
||||
func handleAddLoadingIndicator() async
|
||||
func handleRemoveLoadingIndicator() async
|
||||
|
@ -42,6 +42,7 @@ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category:
|
|||
class TimelineLikeController<Item: Sendable> {
|
||||
|
||||
private unowned var delegate: any TimelineLikeControllerDelegate<Item>
|
||||
private unowned var dataSource: any TimelineLikeControllerDataSource<Item>
|
||||
private let ownerType: String
|
||||
|
||||
@AsyncObservable private(set) var state = State.notLoadedInitial {
|
||||
|
@ -54,8 +55,9 @@ class TimelineLikeController<Item: Sendable> {
|
|||
}
|
||||
}
|
||||
|
||||
init(delegate: any TimelineLikeControllerDelegate<Item>, ownerType: String) {
|
||||
init(delegate: any TimelineLikeControllerDelegate<Item>, dataSource: any TimelineLikeControllerDataSource<Item>, ownerType: String) {
|
||||
self.delegate = delegate
|
||||
self.dataSource = dataSource
|
||||
self.ownerType = ownerType
|
||||
}
|
||||
|
||||
|
@ -81,7 +83,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
await emit(event: .addLoadingIndicator)
|
||||
state = .loadingInitial(token, hasAddedLoadingIndicator: true)
|
||||
do {
|
||||
let items = try await delegate.loadInitial()
|
||||
let items = try await dataSource.loadInitial()
|
||||
guard case .loadingInitial(token, _) = state else {
|
||||
return
|
||||
}
|
||||
|
@ -118,7 +120,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
let token = LoadAttemptToken()
|
||||
state = .loadingNewer(token)
|
||||
do {
|
||||
let items = try await delegate.loadNewer()
|
||||
let items = try await dataSource.loadNewer()
|
||||
guard case .loadingNewer(token) = state else {
|
||||
return
|
||||
}
|
||||
|
@ -137,7 +139,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
return
|
||||
}
|
||||
let token = LoadAttemptToken()
|
||||
guard await delegate.canLoadOlder(),
|
||||
guard await dataSource.canLoadOlder(),
|
||||
// Make sure we're still in the idle state before continuing on, since that may have chnaged while waiting for user input.
|
||||
// If the load more cell appears, then the users scrolls up and back down, the VC may kick off a second loadOlder task
|
||||
// but we only want one to proceed. The actor prevents a data race, and this prevents multiple simultaneousl loadOlder tasks from running.
|
||||
|
@ -148,7 +150,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
await emit(event: .addLoadingIndicator)
|
||||
state = .loadingOlder(token, hasAddedLoadingIndicator: true)
|
||||
do {
|
||||
let items = try await delegate.loadOlder()
|
||||
let items = try await dataSource.loadOlder()
|
||||
guard case .loadingOlder(token, _) = state else {
|
||||
return
|
||||
}
|
||||
|
@ -171,7 +173,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
let token = LoadAttemptToken()
|
||||
state = .loadingGap(token, direction)
|
||||
do {
|
||||
let items = try await delegate.loadGap(in: direction)
|
||||
let items = try await dataSource.loadGap(in: direction)
|
||||
guard case .loadingGap(token, direction) = state else {
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue