Compare commits
No commits in common. "a38c89a17fdc519715ae9ea08389d4d39f0a0c86" and "426b31d46cdd12b53292f42fada59b52f3b1ee8f" have entirely different histories.
a38c89a17f
...
426b31d46c
|
@ -35,7 +35,6 @@
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
||||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||||
|
@ -215,7 +214,6 @@
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; };
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; };
|
||||||
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
|
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
|
||||||
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; };
|
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; };
|
||||||
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */; };
|
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
|
||||||
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; };
|
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; };
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
|
||||||
|
@ -382,7 +380,6 @@
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
@ -561,7 +558,6 @@
|
||||||
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
||||||
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
||||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
|
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
||||||
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
|
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
|
||||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
||||||
|
@ -899,7 +895,6 @@
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
||||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
|
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
||||||
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
|
|
||||||
);
|
);
|
||||||
path = Timeline;
|
path = Timeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1303,7 +1298,6 @@
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */,
|
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
|
@ -1892,7 +1886,6 @@
|
||||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */,
|
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */,
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||||
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
|
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||||
D622757824EE133700B82A16 /* ComposeAssetPicker.swift in Sources */,
|
D622757824EE133700B82A16 /* ComposeAssetPicker.swift in Sources */,
|
||||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
||||||
|
@ -1907,7 +1900,6 @@
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
//
|
|
||||||
// PublicTimelineDescriptionCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/1/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class PublicTimelineDescriptionCollectionViewCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
|
||||||
|
|
||||||
var local = false {
|
|
||||||
didSet {
|
|
||||||
updateLabel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var didDismiss: (() -> Void)?
|
|
||||||
|
|
||||||
private let label = UILabel()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
contentView.backgroundColor = .tintColor
|
|
||||||
label.font = .preferredFont(forTextStyle: .body)
|
|
||||||
label.numberOfLines = 0
|
|
||||||
label.textColor = .white
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(label)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
label.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1),
|
|
||||||
contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1),
|
|
||||||
label.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1),
|
|
||||||
contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: label.bottomAnchor, multiplier: 1),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateLabel() {
|
|
||||||
let str = NSMutableAttributedString()
|
|
||||||
let instanceStr = NSAttributedString(string: mastodonController.instanceURL.host!, attributes: [
|
|
||||||
.font: UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
|
|
||||||
])
|
|
||||||
if local {
|
|
||||||
str.append(NSAttributedString(string: "The local timeline shows public posts from only "))
|
|
||||||
str.append(instanceStr)
|
|
||||||
str.append(NSAttributedString(string: "."))
|
|
||||||
} else {
|
|
||||||
str.append(NSAttributedString(string: "The federated timeline shows public posts from all users that "))
|
|
||||||
str.append(instanceStr)
|
|
||||||
str.append(NSAttributedString(string: " knows about."))
|
|
||||||
}
|
|
||||||
label.attributedText = str
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -14,21 +14,20 @@ import SwiftSoup
|
||||||
|
|
||||||
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
||||||
|
|
||||||
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController {
|
class TimelineViewController: UIViewController {
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private(set) var controller: TimelineLikeController<TimelineItem>!
|
private var controller: TimelineLikeController<TimelineItem>!
|
||||||
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
private var confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
// stored separately because i don't want to query the snapshot every time the user scrolls
|
|
||||||
private var isShowingTimelineDescription = false
|
|
||||||
|
|
||||||
var collectionView: UICollectionView {
|
private var collectionView: UICollectionView {
|
||||||
view as! UICollectionView
|
view as! UICollectionView
|
||||||
}
|
}
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
|
@ -62,14 +61,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
// TODO: delegates
|
// TODO: delegates
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
// collectionView.dragDelegate = self
|
// collectionView.dragDelegate = self
|
||||||
|
|
||||||
registerTimelineLikeCells()
|
|
||||||
collectionView.register(PublicTimelineDescriptionCollectionViewCell.self, forCellWithReuseIdentifier: "publicTimelineDescription")
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction(handler: { [unowned self] _ in
|
let refreshControl = UIRefreshControl(frame: .zero, primaryAction: UIAction(handler: { [unowned self] _ in
|
||||||
Task {
|
Task {
|
||||||
await self.controller.loadNewer()
|
await self.controller.loadNewer()
|
||||||
|
@ -77,7 +74,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
collectionView.refreshControl = refreshControl
|
collectionView.refreshControl = refreshControl
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
@ -97,40 +94,34 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
config.text = try! doc.text()
|
config.text = try! doc.text()
|
||||||
cell.contentConfiguration = config
|
cell.contentConfiguration = config
|
||||||
}
|
}
|
||||||
let timelineDescriptionCell = UICollectionView.CellRegistration<PublicTimelineDescriptionCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
|
||||||
guard case .public(let local) = timeline else {
|
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
cell.mastodonController = self.mastodonController
|
|
||||||
cell.local = local
|
|
||||||
cell.didDismiss = { [unowned self] in
|
|
||||||
self.removeTimelineDescriptionCell()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case .status(_, _):
|
case .status(_, _):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)
|
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
return loadingIndicatorCell(for: indexPath)
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "loadingIndicator", for: indexPath) as! LoadingCollectionViewCell
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
return cell
|
||||||
case .confirmLoadMore:
|
case .confirmLoadMore:
|
||||||
return confirmLoadMoreCell(for: indexPath)
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "confirmLoadMore", for: indexPath) as! ConfirmLoadMoreCollectionViewCell
|
||||||
case .publicTimelineDescription:
|
cell.confirmLoadMore = self.confirmLoadMore
|
||||||
self.isShowingTimelineDescription = true
|
Task {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: timelineDescriptionCell, for: indexPath, item: itemIdentifier)
|
if case .loadingOlder(_, _) = await controller.state {
|
||||||
|
cell.isLoading = true
|
||||||
|
} else {
|
||||||
|
cell.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyInitialSnapshot() {
|
private func applyInitialSnapshot() {
|
||||||
if case .public(let local) = timeline,
|
// TODO: this might not be necessary
|
||||||
(local && !Preferences.shared.hasShownLocalTimelineDescription) ||
|
// TODO: yes it is, for public timeline descriptions
|
||||||
(!local && Preferences.shared.hasShownFederatedTimelineDescription) {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.appendSections([.header])
|
|
||||||
snapshot.appendItems([.publicTimelineDescription], toSection: .header)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -141,34 +132,20 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeTimelineDescriptionCell() {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.deleteSections([.header])
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
|
||||||
isShowingTimelineDescription = false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController {
|
extension TimelineViewController {
|
||||||
enum Section: TimelineLikeCollectionViewSection {
|
enum Section: Hashable {
|
||||||
case header
|
case header
|
||||||
case statuses
|
case statuses
|
||||||
case footer
|
case footer
|
||||||
|
|
||||||
static var entries: Self { .statuses }
|
|
||||||
}
|
}
|
||||||
enum Item: TimelineLikeCollectionViewItem {
|
enum Item: Hashable {
|
||||||
typealias TimelineItem = String // status ID
|
|
||||||
|
|
||||||
case status(id: String, state: StatusState)
|
case status(id: String, state: StatusState)
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
case confirmLoadMore
|
case confirmLoadMore
|
||||||
case publicTimelineDescription
|
// // TODO: remove local param from this
|
||||||
|
// case publicTimelineDescription(local: Bool)
|
||||||
static func fromTimelineItem(_ id: String) -> Self {
|
|
||||||
return .status(id: id, state: .unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
@ -178,8 +155,8 @@ extension TimelineViewController {
|
||||||
return true
|
return true
|
||||||
case (.confirmLoadMore, .confirmLoadMore):
|
case (.confirmLoadMore, .confirmLoadMore):
|
||||||
return true
|
return true
|
||||||
case (.publicTimelineDescription, .publicTimelineDescription):
|
// case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
|
||||||
return true
|
// return a == b
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -194,14 +171,15 @@ extension TimelineViewController {
|
||||||
hasher.combine(1)
|
hasher.combine(1)
|
||||||
case .confirmLoadMore:
|
case .confirmLoadMore:
|
||||||
hasher.combine(2)
|
hasher.combine(2)
|
||||||
case .publicTimelineDescription:
|
// case .publicTimelineDescription(local: let local):
|
||||||
hasher.combine(3)
|
// hasher.combine(3)
|
||||||
|
// hasher.combine(local)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hideSeparators: Bool {
|
var hideSeparators: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .loadingIndicator, .publicTimelineDescription:
|
case .loadingIndicator:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -210,8 +188,7 @@ extension TimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: TimelineLikeControllerDelegate
|
extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||||
extension TimelineViewController {
|
|
||||||
typealias TimelineItem = String // status ID
|
typealias TimelineItem = String // status ID
|
||||||
|
|
||||||
func loadInitial() async throws -> [TimelineItem] {
|
func loadInitial() async throws -> [TimelineItem] {
|
||||||
|
@ -219,6 +196,8 @@ extension TimelineViewController {
|
||||||
throw Error.noClient
|
throw Error.noClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline)
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
let (statuses, _) = try await mastodonController.run(request)
|
||||||
|
|
||||||
|
@ -260,6 +239,8 @@ extension TimelineViewController {
|
||||||
throw Error.noOlder
|
throw Error.noOlder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
let (statuses, _) = try await mastodonController.run(request)
|
||||||
|
|
||||||
|
@ -274,7 +255,120 @@ extension TimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Error: TimelineLikeCollectionViewError {
|
func canLoadOlder() async -> Bool {
|
||||||
|
if Preferences.shared.disableInfiniteScrolling {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||||
|
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||||
|
snapshot.appendSections([.footer])
|
||||||
|
}
|
||||||
|
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
for await _ in self.confirmLoadMore.values {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fatalError("unreachable")
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
||||||
|
switch event {
|
||||||
|
case .addLoadingIndicator:
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if !snapshot.sectionIdentifiers.contains(.footer) {
|
||||||
|
snapshot.appendSections([.footer])
|
||||||
|
}
|
||||||
|
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||||
|
snapshot.reconfigureItems([.confirmLoadMore])
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
||||||
|
}
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
case .removeLoadingIndicator:
|
||||||
|
let oldContentOffset = collectionView.contentOffset
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.footer])
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
collectionView.contentOffset = oldContentOffset
|
||||||
|
|
||||||
|
case .loadAllError(let error, _):
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] (toast: ToastView) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
Task {
|
||||||
|
await self?.controller.loadInitial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
case .replaceAllItems(let ids, _):
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.sectionIdentifiers.contains(.statuses) {
|
||||||
|
snapshot.deleteSections([.statuses])
|
||||||
|
}
|
||||||
|
snapshot.appendSections([.statuses])
|
||||||
|
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
case .loadNewerError(Error.allCaughtUp, _):
|
||||||
|
var config = ToastConfiguration(title: "You're all caught up")
|
||||||
|
config.edge = .top
|
||||||
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
config.action = { (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
case .loadNewerError(let error, _):
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] (toast: ToastView) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
Task {
|
||||||
|
await self?.controller.loadNewer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
case .prependItems(let ids, _):
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
let items = ids.map { Item.status(id: $0, state: .unknown) }
|
||||||
|
let first = snapshot.itemIdentifiers(inSection: .statuses).first
|
||||||
|
if let first {
|
||||||
|
snapshot.insertItems(items, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(items, toSection: .statuses)
|
||||||
|
}
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
if let first,
|
||||||
|
let indexPath = dataSource.indexPath(for: first) {
|
||||||
|
// TODO: i can't tell if this actually works or not
|
||||||
|
// maintain the current position in the list (don't scroll to top)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .loadOlderError(let error, _):
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] (toast: ToastView) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
Task {
|
||||||
|
await self?.controller.loadOlder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
case .appendItems(let ids, _):
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
||||||
|
snapshot.deleteItems([.confirmLoadMore])
|
||||||
|
}
|
||||||
|
snapshot.appendItems(ids.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error {
|
||||||
case noClient
|
case noClient
|
||||||
case noNewer
|
case noNewer
|
||||||
case noOlder
|
case noOlder
|
||||||
|
@ -296,24 +390,7 @@ extension TimelineViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
extension TimelineViewController: ToastableViewController {
|
||||||
return
|
|
||||||
}
|
|
||||||
switch item {
|
|
||||||
case .publicTimelineDescription:
|
|
||||||
removeTimelineDescriptionCell()
|
|
||||||
|
|
||||||
default:
|
|
||||||
// TODO: cell selection
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
||||||
if isShowingTimelineDescription {
|
|
||||||
removeTimelineDescriptionCell()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,10 @@ class TimelinesPageViewController: SegmentedPageViewController {
|
||||||
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
||||||
home.title = homeTitle
|
home.title = homeTitle
|
||||||
|
|
||||||
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
let federated = TimelineTableViewController(for: .public(local: false), mastodonController: mastodonController)
|
||||||
federated.title = federatedTitle
|
federated.title = federatedTitle
|
||||||
|
|
||||||
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
let local = TimelineTableViewController(for: .public(local: true), mastodonController: mastodonController)
|
||||||
local.title = localTitle
|
local.title = localTitle
|
||||||
|
|
||||||
super.init(titles: [
|
super.init(titles: [
|
||||||
|
|
|
@ -1,223 +0,0 @@
|
||||||
//
|
|
||||||
// TimelineLikeCollectionViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/24/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeControllerDelegate, ToastableViewController {
|
|
||||||
associatedtype Section: TimelineLikeCollectionViewSection
|
|
||||||
associatedtype Item: TimelineLikeCollectionViewItem where Item.TimelineItem == Self.TimelineItem
|
|
||||||
associatedtype Error: TimelineLikeCollectionViewError
|
|
||||||
|
|
||||||
// this needs to be an IUO because it can't be set until after the super init is called, so that self can be passed as the delegate param
|
|
||||||
var controller: TimelineLikeController<TimelineItem>! { get }
|
|
||||||
var confirmLoadMore: PassthroughSubject<Void, Never> { get }
|
|
||||||
|
|
||||||
var collectionView: UICollectionView { get }
|
|
||||||
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol TimelineLikeCollectionViewSection: Hashable {
|
|
||||||
static var entries: Self { get }
|
|
||||||
static var footer: Self { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol TimelineLikeCollectionViewItem: Hashable {
|
|
||||||
associatedtype TimelineItem
|
|
||||||
|
|
||||||
static var loadingIndicator: Self { get }
|
|
||||||
static var confirmLoadMore: Self { get }
|
|
||||||
|
|
||||||
static func fromTimelineItem(_ item: TimelineItem) -> Self
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: equatable might not be the best for this?
|
|
||||||
protocol TimelineLikeCollectionViewError: Error, Equatable {
|
|
||||||
static var allCaughtUp: Self { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: TimelineLikeControllerDelegate
|
|
||||||
extension TimelineLikeCollectionViewController {
|
|
||||||
func canLoadOlder() async -> Bool {
|
|
||||||
if Preferences.shared.disableInfiniteScrolling {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
|
||||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
|
||||||
snapshot.appendSections([.footer])
|
|
||||||
}
|
|
||||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
for await _ in confirmLoadMore.values {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
fatalError("unreachable")
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async {
|
|
||||||
switch event {
|
|
||||||
case .addLoadingIndicator:
|
|
||||||
await handleAddLoadingIndicator()
|
|
||||||
case .removeLoadingIndicator:
|
|
||||||
await handleRemoveLoadingIndicator()
|
|
||||||
case .loadAllError(let error, _):
|
|
||||||
await handleLoadAllError(error)
|
|
||||||
case .replaceAllItems(let items, _):
|
|
||||||
await handleReplaceAllItems(items)
|
|
||||||
case .loadNewerError(let error, _):
|
|
||||||
await handleLoadNewerError(error)
|
|
||||||
case .prependItems(let items, _):
|
|
||||||
await handlePrependItems(items)
|
|
||||||
case .loadOlderError(let error, _):
|
|
||||||
await handleLoadOlderError(error)
|
|
||||||
case .appendItems(let items, _):
|
|
||||||
await handleAppendItems(items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAddLoadingIndicator() async {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if !snapshot.sectionIdentifiers.contains(.footer) {
|
|
||||||
snapshot.appendSections([.footer])
|
|
||||||
}
|
|
||||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
|
||||||
snapshot.reconfigureItems([.confirmLoadMore])
|
|
||||||
} else {
|
|
||||||
snapshot.appendItems([.loadingIndicator], toSection: .footer)
|
|
||||||
}
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveLoadingIndicator() async {
|
|
||||||
let oldContentOffset = collectionView.contentOffset
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.deleteSections([.footer])
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
// prevent the collection view from scrolling as we remove the loading indicator and add the timeline items
|
|
||||||
collectionView.contentOffset = oldContentOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLoadAllError(_ error: Swift.Error) async {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
Task {
|
|
||||||
await self?.controller.loadInitial()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleReplaceAllItems(_ timelineItems: [TimelineItem]) async {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if snapshot.sectionIdentifiers.contains(.entries) {
|
|
||||||
snapshot.deleteSections([.entries])
|
|
||||||
}
|
|
||||||
snapshot.appendSections([.entries])
|
|
||||||
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLoadNewerError(_ error: Swift.Error) async {
|
|
||||||
var config: ToastConfiguration
|
|
||||||
if let error = error as? Self.Error,
|
|
||||||
error == .allCaughtUp {
|
|
||||||
config = ToastConfiguration(title: "You're all caught up")
|
|
||||||
config.edge = .top
|
|
||||||
config.dismissAutomaticallyAfter = 2
|
|
||||||
config.action = { toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
Task {
|
|
||||||
await self?.controller.loadNewer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePrependItems(_ timelineItems: [TimelineItem]) async {
|
|
||||||
let items = timelineItems.map { Item.fromTimelineItem($0) }
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
let first = snapshot.itemIdentifiers(inSection: .entries).first
|
|
||||||
if let first {
|
|
||||||
snapshot.insertItems(items, beforeItem: first)
|
|
||||||
} else {
|
|
||||||
snapshot.appendItems(items, toSection: .entries)
|
|
||||||
}
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
if let first,
|
|
||||||
let indexPath = dataSource.indexPath(for: first) {
|
|
||||||
// TODO: i can't tell if this actually works or not
|
|
||||||
// maintain the current scroll position in the list (don't scroll to top)
|
|
||||||
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLoadOlderError(_ error: Swift.Error) async {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
Task {
|
|
||||||
await self?.controller.loadOlder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAppendItems(_ timelineItems: [TimelineItem]) async {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
// TODO: this might not be necessary, isn't the confirm item removed separately?
|
|
||||||
if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
|
|
||||||
snapshot.deleteItems([.confirmLoadMore])
|
|
||||||
}
|
|
||||||
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineLikeCollectionViewController {
|
|
||||||
// apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
|
|
||||||
// but we always want to update the data source on the main thread for consistency, so this method does that
|
|
||||||
func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
|
|
||||||
let task = Task { @MainActor in
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
|
||||||
}
|
|
||||||
await task.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerTimelineLikeCells() {
|
|
||||||
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
|
|
||||||
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadingIndicatorCell(for indexPath: IndexPath) -> UICollectionViewCell {
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "loadingIndicator", for: indexPath) as! LoadingCollectionViewCell
|
|
||||||
cell.indicator.startAnimating()
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
func confirmLoadMoreCell(for indexPath: IndexPath) -> UICollectionViewCell {
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "confirmLoadMore", for: indexPath) as! ConfirmLoadMoreCollectionViewCell
|
|
||||||
cell.confirmLoadMore = self.confirmLoadMore
|
|
||||||
Task {
|
|
||||||
if case .loadingOlder(_, _) = await controller.state {
|
|
||||||
cell.isLoading = true
|
|
||||||
} else {
|
|
||||||
cell.isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -29,7 +29,7 @@ actor TimelineLikeController<Item> {
|
||||||
|
|
||||||
unowned var delegate: any TimelineLikeControllerDelegate<Item>
|
unowned var delegate: any TimelineLikeControllerDelegate<Item>
|
||||||
|
|
||||||
private(set) var state = State.notLoadedInitial {
|
private(set) var state = State.idle {
|
||||||
willSet {
|
willSet {
|
||||||
precondition(state.canTransition(to: newValue))
|
precondition(state.canTransition(to: newValue))
|
||||||
logger.debug("State: \(self.state.debugDescription, privacy: .public) -> \(newValue.debugDescription, privacy: .public)")
|
logger.debug("State: \(self.state.debugDescription, privacy: .public) -> \(newValue.debugDescription, privacy: .public)")
|
||||||
|
@ -41,7 +41,7 @@ actor TimelineLikeController<Item> {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadInitial() async {
|
func loadInitial() async {
|
||||||
guard state == .notLoadedInitial else {
|
guard state == .idle else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let token = LoadAttemptToken()
|
let token = LoadAttemptToken()
|
||||||
|
@ -86,7 +86,11 @@ actor TimelineLikeController<Item> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let token = LoadAttemptToken()
|
let token = LoadAttemptToken()
|
||||||
|
// TODO: does the waiting state need to include the token?
|
||||||
|
// TODO: does this even need to be a separate state? maybe we should just await the delegate's permission, since it can suspend until user input. then the prompt could appear, and the user could scroll back to the top and still be able to refresh
|
||||||
|
// state = .waitingForLoadOlderPermission
|
||||||
guard await delegate.canLoadOlder() else {
|
guard await delegate.canLoadOlder() else {
|
||||||
|
// state = .idle
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state = .loadingOlder(token, hasAddedLoadingIndicator: false)
|
state = .loadingOlder(token, hasAddedLoadingIndicator: false)
|
||||||
|
@ -116,22 +120,22 @@ actor TimelineLikeController<Item> {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State: Equatable, CustomDebugStringConvertible {
|
enum State: Equatable, CustomDebugStringConvertible {
|
||||||
case notLoadedInitial
|
|
||||||
case idle
|
case idle
|
||||||
case loadingInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
case loadingInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
||||||
case loadingNewer(LoadAttemptToken)
|
case loadingNewer(LoadAttemptToken)
|
||||||
|
// case waitingForLoadOlderPermission
|
||||||
case loadingOlder(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
case loadingOlder(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
||||||
|
|
||||||
var debugDescription: String {
|
var debugDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .notLoadedInitial:
|
|
||||||
return "notLoadedInitial"
|
|
||||||
case .idle:
|
case .idle:
|
||||||
return "idle"
|
return "idle"
|
||||||
case .loadingInitial(let token, let hasAddedLoadingIndicator):
|
case .loadingInitial(let token, let hasAddedLoadingIndicator):
|
||||||
return "loadingInitial(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
return "loadingInitial(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
||||||
case .loadingNewer(let token):
|
case .loadingNewer(let token):
|
||||||
return "loadingNewer(\(ObjectIdentifier(token)))"
|
return "loadingNewer(\(ObjectIdentifier(token)))"
|
||||||
|
// case .waitingForLoadOlderPermission:
|
||||||
|
// return "waitingForLoadOlderPermission"
|
||||||
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
||||||
return "loadingOlder(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
return "loadingOlder(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
||||||
}
|
}
|
||||||
|
@ -139,16 +143,9 @@ actor TimelineLikeController<Item> {
|
||||||
|
|
||||||
func canTransition(to: State) -> Bool {
|
func canTransition(to: State) -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .notLoadedInitial:
|
|
||||||
switch to {
|
|
||||||
case .loadingInitial(_, hasAddedLoadingIndicator: _):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case .idle:
|
case .idle:
|
||||||
switch to {
|
switch to {
|
||||||
case .loadingNewer(_), .loadingOlder(_, _):
|
case .loadingInitial(_, _), .loadingNewer(_)/*, .waitingForLoadOlderPermission*/, .loadingOlder(_, _):
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -159,6 +156,13 @@ actor TimelineLikeController<Item> {
|
||||||
return to == .idle
|
return to == .idle
|
||||||
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
||||||
return to == .idle || (!hasAddedLoadingIndicator && to == .loadingOlder(token, hasAddedLoadingIndicator: true))
|
return to == .idle || (!hasAddedLoadingIndicator && to == .loadingOlder(token, hasAddedLoadingIndicator: true))
|
||||||
|
// case .waitingForLoadOlderPermission:
|
||||||
|
// switch to {
|
||||||
|
// case .idle, .loadingOlder(_, _):
|
||||||
|
// return true
|
||||||
|
// default:
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue