Compare commits

..

5 Commits

6 changed files with 115 additions and 27 deletions

View File

@ -46,6 +46,9 @@ class MastodonController: ObservableObject {
@Published private(set) var instance: Instance!
private(set) var customEmojis: [Emoji]?
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool {
accountInfo != nil
}
@ -115,17 +118,56 @@ class MastodonController: ObservableObject {
}
}
// todo: this should dedup requests
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0, completion: completion)
}
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread)
if let instance = self.instance {
completion?(instance)
} else {
let request = Client.getInstance()
run(request) { (response) in
guard case let .success(instance, _) = response else { fatalError() }
DispatchQueue.main.async {
self.instance = instance
completion?(instance)
if let completion = completion {
pendingOwnInstanceRequestCallbacks.append(completion)
}
if ownInstanceRequest == nil {
let request = Client.getInstance()
ownInstanceRequest = run(request) { (response) in
switch response {
case .failure(_):
let delay: DispatchTimeInterval
switch retryAttempt {
case 0:
delay = .seconds(1)
case 1:
delay = .seconds(5)
case 2:
delay = .seconds(30)
case 3:
delay = .seconds(60)
default:
// if we've failed four times, just give up :/
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
// completion is nil because in this invocation of getOwnInstanceInternal we've already added it to the pending callbacks array
self.getOwnInstanceInternal(retryAttempt: retryAttempt + 1, completion: nil)
}
case let .success(instance, _):
DispatchQueue.main.async {
self.ownInstanceRequest = nil
self.instance = instance
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(instance)
}
self.pendingOwnInstanceRequestCallbacks = []
}
}
}
}
}

View File

@ -69,6 +69,7 @@ class AssetCollectionViewController: UICollectionViewController {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
collectionView.alwaysBounceVertical = true
collectionView.allowsMultipleSelection = true
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
@ -97,19 +98,6 @@ class AssetCollectionViewController: UICollectionViewController {
}
})
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = fetchAssets(with: options)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.assets])
var items: [Item] = [.showCamera]
fetchResult.enumerateObjects { (asset, _, _) in
items.append(.asset(asset))
}
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
collectionView.allowsMultipleSelection = true
setEditing(true, animated: false)
updateItemsSelectedCount()
@ -122,6 +110,12 @@ class AssetCollectionViewController: UICollectionViewController {
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadAssets()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
@ -137,6 +131,40 @@ class AssetCollectionViewController: UICollectionViewController {
}
}
private func loadAssets() {
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
self.loadAssets()
}
return
case .restricted, .denied:
// todo: better UI for this
return
case .authorized, .limited:
// todo: show "add more" button for limited access
break
@unknown default:
// who knows, just try anyways
break
}
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = fetchAssets(with: options)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.assets])
var items: [Item] = [.showCamera]
fetchResult.enumerateObjects { (asset, _, _) in
items.append(.asset(asset))
}
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
}
open func fetchAssets(with options: PHFetchOptions) -> PHFetchResult<PHAsset> {
return PHAsset.fetchAssets(with: options)
}

View File

@ -118,7 +118,7 @@ class ProfileDirectoryFilterView: UICollectionReusableView {
}
@objc private func filterChanged() {
let scope = Scope(rawValue: scope.selectedSegmentIndex)!
let scope = Scope(rawValue: self.scope.selectedSegmentIndex)!
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
onFilterChanged?(scope, order)
}

View File

@ -94,11 +94,11 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
loadingVC = LoadingViewController()
embedChild(loadingVC!)
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
guard let self = self else { return }
guard let self = self, let image = image else { return }
self.imageRequest = nil
DispatchQueue.main.async {
self.loadingVC?.removeViewAndController()
self.createLargeImage(data: data!, image: image!, url: self.url)
self.createLargeImage(data: data, image: image, url: self.url)
}
}
}

View File

@ -72,7 +72,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
let request = Client.getStatuses(timeline: timeline)
mastodonController?.run(request) { (response) in
guard case let .success(statuses, pagination) = response else { fatalError() }
guard case let .success(statuses, pagination) = response else {
completion([])
return
}
self.newer = pagination?.newer
self.older = pagination?.older
@ -92,7 +95,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
let request = Client.getStatuses(timeline: timeline, range: older)
mastodonController.run(request) { (response) in
guard case let .success(statuses, pagination) = response else { fatalError() }
guard case let .success(statuses, pagination) = response else {
completion([])
return
}
self.older = pagination?.older
@ -111,7 +117,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
let request = Client.getStatuses(timeline: timeline, range: newer)
mastodonController.run(request) { (response) in
guard case let .success(statuses, pagination) = response else { fatalError() }
guard case let .success(statuses, pagination) = response else {
completion([])
return
}
// if there are no new statuses, pagination is nil
// if we were to then overwrite self.newer, future refreshes would fail

View File

@ -65,11 +65,18 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
func loadInitial() {
guard !loaded else { return }
// set loaded immediately so we don't trigger another request while the current one is running
loaded = true
loadInitialItems() { (items) in
guard items.count > 0 else { return }
DispatchQueue.main.async {
guard items.count > 0 else {
// set loaded back to false so the next time the VC appears, we try to load again
// todo: this should probably retry automatically
self.loaded = false
return
}
if self.sections.count < self.headerSectionsCount() {
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
}
@ -97,6 +104,8 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
return "Refresh"
}
// todo: these three should use Result<[Item], Client.Error> so we can differentiate between failed requests and there actually being no results
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
}