Compare commits
4 Commits
426b31d46c
...
a38c89a17f
Author | SHA1 | Date |
---|---|---|
Shadowfacts | a38c89a17f | |
Shadowfacts | 253fb8d27d | |
Shadowfacts | a682c8f5cc | |
Shadowfacts | d18a4b3c42 |
|
@ -35,6 +35,7 @@
|
||||||
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 */; };
|
||||||
|
@ -214,6 +215,7 @@
|
||||||
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 */; };
|
||||||
|
@ -380,6 +382,7 @@
|
||||||
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>"; };
|
||||||
|
@ -558,6 +561,7 @@
|
||||||
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>"; };
|
||||||
|
@ -895,6 +899,7 @@
|
||||||
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>";
|
||||||
|
@ -1298,6 +1303,7 @@
|
||||||
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 */,
|
||||||
|
@ -1886,6 +1892,7 @@
|
||||||
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 */,
|
||||||
|
@ -1900,6 +1907,7 @@
|
||||||
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 */,
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
// 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,20 +14,21 @@ 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 {
|
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController {
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var controller: TimelineLikeController<TimelineItem>!
|
private(set) var controller: TimelineLikeController<TimelineItem>!
|
||||||
private var confirmLoadMore = PassthroughSubject<Void, Never>()
|
let 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
|
||||||
|
|
||||||
private var collectionView: UICollectionView {
|
var collectionView: UICollectionView {
|
||||||
view as! UICollectionView
|
view as! UICollectionView
|
||||||
}
|
}
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
init(for timeline: Timeline, mastodonController: MastodonController!) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
|
@ -61,12 +62,14 @@ class TimelineViewController: UIViewController {
|
||||||
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()
|
||||||
|
@ -74,7 +77,7 @@ class TimelineViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
collectionView.refreshControl = refreshControl
|
collectionView.refreshControl = refreshControl
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
@ -94,34 +97,40 @@ class TimelineViewController: UIViewController {
|
||||||
config.text = try! doc.text()
|
config.text = try! doc.text()
|
||||||
cell.contentConfiguration = config
|
cell.contentConfiguration = config
|
||||||
}
|
}
|
||||||
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
|
let timelineDescriptionCell = UICollectionView.CellRegistration<PublicTimelineDescriptionCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
||||||
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
|
guard case .public(let local) = timeline else {
|
||||||
|
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:
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "loadingIndicator", for: indexPath) as! LoadingCollectionViewCell
|
return loadingIndicatorCell(for: indexPath)
|
||||||
cell.indicator.startAnimating()
|
|
||||||
return cell
|
|
||||||
case .confirmLoadMore:
|
case .confirmLoadMore:
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "confirmLoadMore", for: indexPath) as! ConfirmLoadMoreCollectionViewCell
|
return confirmLoadMoreCell(for: indexPath)
|
||||||
cell.confirmLoadMore = self.confirmLoadMore
|
case .publicTimelineDescription:
|
||||||
Task {
|
self.isShowingTimelineDescription = true
|
||||||
if case .loadingOlder(_, _) = await controller.state {
|
return collectionView.dequeueConfiguredReusableCell(using: timelineDescriptionCell, for: indexPath, item: itemIdentifier)
|
||||||
cell.isLoading = true
|
|
||||||
} else {
|
|
||||||
cell.isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cell
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyInitialSnapshot() {
|
private func applyInitialSnapshot() {
|
||||||
// TODO: this might not be necessary
|
if case .public(let local) = timeline,
|
||||||
// TODO: yes it is, for public timeline descriptions
|
(local && !Preferences.shared.hasShownLocalTimelineDescription) ||
|
||||||
|
(!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) {
|
||||||
|
@ -132,20 +141,34 @@ class TimelineViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func removeTimelineDescriptionCell() {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
isShowingTimelineDescription = false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController {
|
extension TimelineViewController {
|
||||||
enum Section: Hashable {
|
enum Section: TimelineLikeCollectionViewSection {
|
||||||
case header
|
case header
|
||||||
case statuses
|
case statuses
|
||||||
case footer
|
case footer
|
||||||
|
|
||||||
|
static var entries: Self { .statuses }
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: TimelineLikeCollectionViewItem {
|
||||||
|
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
|
||||||
// // TODO: remove local param from this
|
case publicTimelineDescription
|
||||||
// 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) {
|
||||||
|
@ -155,8 +178,8 @@ extension TimelineViewController {
|
||||||
return true
|
return true
|
||||||
case (.confirmLoadMore, .confirmLoadMore):
|
case (.confirmLoadMore, .confirmLoadMore):
|
||||||
return true
|
return true
|
||||||
// case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
|
case (.publicTimelineDescription, .publicTimelineDescription):
|
||||||
// return a == b
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -171,15 +194,14 @@ extension TimelineViewController {
|
||||||
hasher.combine(1)
|
hasher.combine(1)
|
||||||
case .confirmLoadMore:
|
case .confirmLoadMore:
|
||||||
hasher.combine(2)
|
hasher.combine(2)
|
||||||
// case .publicTimelineDescription(local: let local):
|
case .publicTimelineDescription:
|
||||||
// hasher.combine(3)
|
hasher.combine(3)
|
||||||
// hasher.combine(local)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hideSeparators: Bool {
|
var hideSeparators: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .loadingIndicator:
|
case .loadingIndicator, .publicTimelineDescription:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -188,7 +210,8 @@ extension TimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: TimelineLikeControllerDelegate {
|
// MARK: TimelineLikeControllerDelegate
|
||||||
|
extension TimelineViewController {
|
||||||
typealias TimelineItem = String // status ID
|
typealias TimelineItem = String // status ID
|
||||||
|
|
||||||
func loadInitial() async throws -> [TimelineItem] {
|
func loadInitial() async throws -> [TimelineItem] {
|
||||||
|
@ -196,8 +219,6 @@ extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||||
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)
|
||||||
|
|
||||||
|
@ -239,8 +260,6 @@ extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||||
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)
|
||||||
|
|
||||||
|
@ -255,120 +274,7 @@ extension TimelineViewController: TimelineLikeControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func canLoadOlder() async -> Bool {
|
enum Error: TimelineLikeCollectionViewError {
|
||||||
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
|
||||||
|
@ -390,7 +296,24 @@ extension TimelineViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
extension TimelineViewController: ToastableViewController {
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
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 = TimelineTableViewController(for: .public(local: false), mastodonController: mastodonController)
|
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
||||||
federated.title = federatedTitle
|
federated.title = federatedTitle
|
||||||
|
|
||||||
let local = TimelineTableViewController(for: .public(local: true), mastodonController: mastodonController)
|
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
||||||
local.title = localTitle
|
local.title = localTitle
|
||||||
|
|
||||||
super.init(titles: [
|
super.init(titles: [
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
//
|
||||||
|
// 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.idle {
|
private(set) var state = State.notLoadedInitial {
|
||||||
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 == .idle else {
|
guard state == .notLoadedInitial else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let token = LoadAttemptToken()
|
let token = LoadAttemptToken()
|
||||||
|
@ -86,11 +86,7 @@ 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)
|
||||||
|
@ -120,22 +116,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))"
|
||||||
}
|
}
|
||||||
|
@ -143,9 +139,16 @@ 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 .loadingInitial(_, _), .loadingNewer(_)/*, .waitingForLoadOlderPermission*/, .loadingOlder(_, _):
|
case .loadingNewer(_), .loadingOlder(_, _):
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -156,13 +159,6 @@ 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