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!
|
@Published private(set) var instance: Instance!
|
||||||
private(set) var customEmojis: [Emoji]?
|
private(set) var customEmojis: [Emoji]?
|
||||||
|
|
||||||
|
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
||||||
|
private var ownInstanceRequest: URLSessionTask?
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
accountInfo != nil
|
accountInfo != nil
|
||||||
}
|
}
|
||||||
|
@ -115,17 +118,56 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: this should dedup requests
|
|
||||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
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 {
|
if let instance = self.instance {
|
||||||
completion?(instance)
|
completion?(instance)
|
||||||
} else {
|
} else {
|
||||||
let request = Client.getInstance()
|
if let completion = completion {
|
||||||
run(request) { (response) in
|
pendingOwnInstanceRequestCallbacks.append(completion)
|
||||||
guard case let .success(instance, _) = response else { fatalError() }
|
}
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.instance = instance
|
if ownInstanceRequest == nil {
|
||||||
completion?(instance)
|
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))
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
||||||
|
|
||||||
collectionView.alwaysBounceVertical = true
|
collectionView.alwaysBounceVertical = true
|
||||||
|
collectionView.allowsMultipleSelection = true
|
||||||
|
|
||||||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
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)
|
setEditing(true, animated: false)
|
||||||
|
|
||||||
updateItemsSelectedCount()
|
updateItemsSelectedCount()
|
||||||
|
@ -122,6 +110,12 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
loadAssets()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillLayoutSubviews() {
|
override func viewWillLayoutSubviews() {
|
||||||
super.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> {
|
open func fetchAssets(with options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||||
return PHAsset.fetchAssets(with: options)
|
return PHAsset.fetchAssets(with: options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ class ProfileDirectoryFilterView: UICollectionReusableView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func filterChanged() {
|
@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
|
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
||||||
onFilterChanged?(scope, order)
|
onFilterChanged?(scope, order)
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,11 +94,11 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
loadingVC = LoadingViewController()
|
loadingVC = LoadingViewController()
|
||||||
embedChild(loadingVC!)
|
embedChild(loadingVC!)
|
||||||
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
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
|
self.imageRequest = nil
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingVC?.removeViewAndController()
|
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)
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
|
|
||||||
mastodonController?.run(request) { (response) in
|
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.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
@ -92,7 +95,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
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
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
@ -111,7 +117,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
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 there are no new statuses, pagination is nil
|
||||||
// if we were to then overwrite self.newer, future refreshes would fail
|
// if we were to then overwrite self.newer, future refreshes would fail
|
||||||
|
|
|
@ -65,11 +65,18 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
||||||
|
|
||||||
func loadInitial() {
|
func loadInitial() {
|
||||||
guard !loaded else { return }
|
guard !loaded else { return }
|
||||||
|
// set loaded immediately so we don't trigger another request while the current one is running
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
loadInitialItems() { (items) in
|
loadInitialItems() { (items) in
|
||||||
guard items.count > 0 else { return }
|
|
||||||
DispatchQueue.main.async {
|
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() {
|
if self.sections.count < self.headerSectionsCount() {
|
||||||
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
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"
|
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) {
|
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
|
||||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue