Compare commits
5 Commits
1e7bfac13c
...
13a4221fce
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 13a4221fce | |
Shadowfacts | a896573a5e | |
Shadowfacts | edd89450aa | |
Shadowfacts | 5f5ef8fcea | |
Shadowfacts | a3b59c990b |
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue