Compare commits

...

2 Commits

7 changed files with 65 additions and 31 deletions

View File

@ -8,13 +8,13 @@
import UIKit
class AccountListViewController: UIViewController {
class AccountListViewController: UIViewController, CollectionViewController {
typealias Item = String
private let mastodonController: MastodonController
private let accountIDs: [String]
private var collectionView: UICollectionView {
var collectionView: UICollectionView! {
view as! UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
@ -61,9 +61,7 @@ class AccountListViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
collectionView.indexPathsForSelectedItems?.forEach {
collectionView.deselectItem(at: $0, animated: true)
}
clearSelectionOnAppear(animated: animated)
}
}

View File

@ -12,11 +12,11 @@ import Pachyderm
import CoreData
import WebURLFoundationExtras
class ExploreViewController: UIViewController, UICollectionViewDelegate {
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
weak var mastodonController: MastodonController!
private var collectionView: UICollectionView!
var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private(set) var resultsController: SearchResultsViewController!
@ -95,15 +95,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Can't use UICollectionViewController's builtin version of this because it requires
// the collection view layout be passed into the constructor. Swipe actions for list collection views
// are created by passing a closure to the layout's configuration. This closure needs to capture
// `self`, so it can't be passed into the super constructor.
if let indexPaths = collectionView.indexPathsForSelectedItems {
for indexPath in indexPaths {
collectionView.deselectItem(at: indexPath, animated: true)
}
}
clearSelectionOnAppear(animated: animated)
}
override func viewDidAppear(_ animated: Bool) {

View File

@ -10,7 +10,7 @@ import UIKit
import Pachyderm
import Combine
class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionViewController {
class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
weak var owner: ProfileViewController?
let mastodonController: MastodonController
@ -188,9 +188,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
collectionView.indexPathsForSelectedItems?.forEach {
collectionView.deselectItem(at: $0, animated: true)
}
clearSelectionOnAppear(animated: animated)
Task {
if case .notLoadedInitial = controller.state {

View File

@ -9,7 +9,7 @@
import UIKit
import Pachyderm
class StatusActionAccountListViewController: UIViewController {
class StatusActionAccountListViewController: UIViewController, CollectionViewController {
private let mastodonController: MastodonController
private let actionType: ActionType
@ -20,8 +20,8 @@ class StatusActionAccountListViewController: UIViewController {
/// If `true`, a warning will be shown below the account list describing that the total favs/reblogs may be innacurate.
var showInacurateCountWarning = false
private var collectionView: UICollectionView {
view as! UICollectionView
var collectionView: UICollectionView! {
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
@ -120,9 +120,7 @@ class StatusActionAccountListViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
collectionView.indexPathsForSelectedItems?.forEach {
collectionView.deselectItem(at: $0, animated: true)
}
clearSelectionOnAppear(animated: animated)
if accountIDs == nil {
Task {

View File

@ -210,9 +210,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
collectionView.indexPathsForSelectedItems?.forEach {
collectionView.deselectItem(at: $0, animated: true)
}
clearSelectionOnAppear(animated: animated)
if case .notLoadedInitial = controller.state {
if restoreState() {

View File

@ -11,3 +11,23 @@ import UIKit
protocol CollectionViewController: UIViewController {
var collectionView: UICollectionView! { get }
}
extension CollectionViewController {
func clearSelectionOnAppear(animated: Bool) {
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else {
return
}
if let transitionCoordinator {
transitionCoordinator.animate { context in
self.collectionView.deselectItem(at: indexPath, animated: true)
} completion: { context in
if context.isCancelled {
self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [] /* UICollectionViewScrollPositionNone */)
}
}
} else {
collectionView.deselectItem(at: indexPath, animated: animated)
}
}
}

View File

@ -8,6 +8,7 @@
import UIKit
import Pachyderm
import Sentry
struct ToastConfiguration {
var systemImageName: String?
@ -38,7 +39,7 @@ extension ToastConfiguration {
init(from error: Error, with title: String, in viewController: UIViewController, retryAction: ((ToastView) -> Void)?) {
self.init(title: title)
// localizedDescription is statically dispatched, so we need to call it after the downcast
if let error = error as? Client.Error {
if let error = error as? Pachyderm.Client.Error {
self.subtitle = error.localizedDescription
self.systemImageName = error.systemImageName
self.longPressAction = { [unowned viewController] toast in
@ -54,6 +55,35 @@ extension ToastConfiguration {
})
viewController.present(reporter, animated: true)
}
// TODO: this is a bizarre place to do this, but code path covers basically all errors
switch error.type {
case .invalidRequest, .invalidResponse, .invalidModel(_), .mastodonError(_):
SentrySDK.capture(error: error) { scope in
let crumb = Breadcrumb(level: .error, category: "error")
crumb.message = title
crumb.data = [
"request_method": error.requestMethod.name,
"request_endpoint": error.requestEndpoint.description,
]
switch error.type {
case .invalidRequest:
crumb.data!["error_type"] = "invalid_request"
case .invalidResponse:
crumb.data!["error_type"] = "invalid_response"
case .invalidModel(let error):
crumb.data!["error_type"] = "invalid_model"
crumb.data!["underlying_error"] = String(describing: error)
case .mastodonError(let error):
crumb.data!["error_type"] = "mastodon_error"
crumb.data!["underlying_error"] = error
default:
break
}
scope.add(crumb)
}
default:
break
}
} else {
self.subtitle = error.localizedDescription
self.systemImageName = "exclamationmark.triangle"
@ -73,7 +103,7 @@ extension ToastConfiguration {
}
}
fileprivate extension Client.Error {
fileprivate extension Pachyderm.Client.Error {
var systemImageName: String {
switch type {
case .networkError(_):