Compare commits

...

2 Commits

7 changed files with 65 additions and 31 deletions

View File

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

View File

@ -12,11 +12,11 @@ import Pachyderm
import CoreData import CoreData
import WebURLFoundationExtras import WebURLFoundationExtras
class ExploreViewController: UIViewController, UICollectionViewDelegate { class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var collectionView: UICollectionView! var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private(set) var resultsController: SearchResultsViewController! private(set) var resultsController: SearchResultsViewController!
@ -95,15 +95,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// Can't use UICollectionViewController's builtin version of this because it requires clearSelectionOnAppear(animated: animated)
// 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)
}
}
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {

View File

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

View File

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

View File

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

View File

@ -11,3 +11,23 @@ import UIKit
protocol CollectionViewController: UIViewController { protocol CollectionViewController: UIViewController {
var collectionView: UICollectionView! { get } 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 UIKit
import Pachyderm import Pachyderm
import Sentry
struct ToastConfiguration { struct ToastConfiguration {
var systemImageName: String? var systemImageName: String?
@ -38,7 +39,7 @@ extension ToastConfiguration {
init(from error: Error, with title: String, in viewController: UIViewController, retryAction: ((ToastView) -> Void)?) { init(from error: Error, with title: String, in viewController: UIViewController, retryAction: ((ToastView) -> Void)?) {
self.init(title: title) self.init(title: title)
// localizedDescription is statically dispatched, so we need to call it after the downcast // 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.subtitle = error.localizedDescription
self.systemImageName = error.systemImageName self.systemImageName = error.systemImageName
self.longPressAction = { [unowned viewController] toast in self.longPressAction = { [unowned viewController] toast in
@ -54,6 +55,35 @@ extension ToastConfiguration {
}) })
viewController.present(reporter, animated: true) 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 { } else {
self.subtitle = error.localizedDescription self.subtitle = error.localizedDescription
self.systemImageName = "exclamationmark.triangle" self.systemImageName = "exclamationmark.triangle"
@ -73,7 +103,7 @@ extension ToastConfiguration {
} }
} }
fileprivate extension Client.Error { fileprivate extension Pachyderm.Client.Error {
var systemImageName: String { var systemImageName: String {
switch type { switch type {
case .networkError(_): case .networkError(_):