Fix crash when conversation loading fails
This commit is contained in:
parent
30ef9cc6d0
commit
b917120f17
|
@ -36,6 +36,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
var visibilityBarButtonItem: UIBarButtonItem!
|
var visibilityBarButtonItem: UIBarButtonItem!
|
||||||
|
|
||||||
|
private var loadingState = LoadingState.unloaded
|
||||||
|
|
||||||
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
|
init(for mainStatusID: String, state: StatusState = .unknown, mastodonController: MastodonController) {
|
||||||
self.mainStatusID = mainStatusID
|
self.mainStatusID = mainStatusID
|
||||||
self.mainStatusState = state
|
self.mainStatusState = state
|
||||||
|
@ -136,9 +138,12 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadMainStatus() {
|
private func loadMainStatus() {
|
||||||
|
guard loadingState == .unloaded else { return }
|
||||||
|
|
||||||
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||||
self.mainStatusLoaded(mainStatus)
|
self.mainStatusLoaded(mainStatus)
|
||||||
} else {
|
} else {
|
||||||
|
loadingState = .loadingMain
|
||||||
let request = Client.getStatus(id: mainStatusID)
|
let request = Client.getStatus(id: mainStatusID)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
|
@ -148,14 +153,24 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
self.mainStatusLoaded(statusMO)
|
self.mainStatusLoaded(statusMO)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(_):
|
case let .failure(error):
|
||||||
fatalError()
|
DispatchQueue.main.async {
|
||||||
|
self.loadingState = .unloaded
|
||||||
|
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Status") { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.loadMainStatus()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
||||||
|
mainStatus.incrementReferenceCount()
|
||||||
|
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
@ -163,47 +178,77 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
let mainStatusInReplyToID = mainStatus.inReplyToID
|
loadingState = .loadedMain
|
||||||
mainStatus.incrementReferenceCount()
|
|
||||||
|
|
||||||
|
loadContext(for: mainStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadContext(for mainStatus: StatusMO) {
|
||||||
|
guard loadingState == .loadedMain else { return }
|
||||||
|
|
||||||
|
loadingState = .loadingContext
|
||||||
|
|
||||||
|
// save the id here because we can't access the MO from the whatever thread the network callback happens on
|
||||||
|
let mainStatusInReplyToID = mainStatus.inReplyToID
|
||||||
|
|
||||||
// todo: it would be nice to cache these contexts
|
// todo: it would be nice to cache these contexts
|
||||||
let request = Status.getContext(mainStatusID)
|
let request = Status.getContext(mainStatusID)
|
||||||
mastodonController.run(request) { response in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(context, _) = response else { fatalError() }
|
switch response {
|
||||||
|
case let .success(context, _):
|
||||||
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||||
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||||
|
|
||||||
// todo: should this really be blindly adding all the descendants?
|
// todo: should this really be blindly adding all the descendants?
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .failure(error):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
var snapshot = self.dataSource.snapshot()
|
self.loadingState = .loadedMain
|
||||||
snapshot.insertItems(parents.map { .status(id: $0, state: .unknown) }, beforeItem: mainStatusItem)
|
|
||||||
|
|
||||||
// fetch all descendant status managed objects
|
let config = ToastConfiguration(from: error, with: "Error Loading Content") { [weak self] (toast) in
|
||||||
let descendantIDs = context.descendants.map(\.id)
|
toast.dismissToast(animated: true)
|
||||||
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
self?.loadContext(for: mainStatus)
|
||||||
request.predicate = NSPredicate(format: "id in %@", descendantIDs)
|
|
||||||
|
|
||||||
if let descendants = try? self.mastodonController.persistentContainer.viewContext.fetch(request) {
|
|
||||||
// convert array of descendant statuses into tree of sub-threads
|
|
||||||
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
|
||||||
|
|
||||||
// convert sub-threads into items for section and add to snapshot
|
|
||||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
|
||||||
// ensure that the main status is on-screen after newly loaded statuses are added
|
|
||||||
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
|
||||||
if let indexPath = self.dataSource.indexPath(for: mainStatusItem) {
|
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func contextLoaded(mainStatus: StatusMO, context: ConversationContext, parentIDs: [String]) {
|
||||||
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.insertItems(parentIDs.map { .status(id: $0, state: .unknown) }, beforeItem: mainStatusItem)
|
||||||
|
|
||||||
|
// fetch all descendant status managed objects
|
||||||
|
let descendantIDs = context.descendants.map(\.id)
|
||||||
|
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
||||||
|
request.predicate = NSPredicate(format: "id in %@", descendantIDs)
|
||||||
|
|
||||||
|
if let descendants = try? self.mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||||
|
// convert array of descendant statuses into tree of sub-threads
|
||||||
|
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||||
|
|
||||||
|
// convert sub-threads into items for section and add to snapshot
|
||||||
|
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
|
// ensure that the main status is on-screen after newly loaded statuses are added
|
||||||
|
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
||||||
|
if let indexPath = self.dataSource.indexPath(for: mainStatusItem) {
|
||||||
|
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadingState = .loadedAll
|
||||||
|
}
|
||||||
|
|
||||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||||
var statuses = statuses
|
var statuses = statuses
|
||||||
|
@ -390,6 +435,16 @@ extension ConversationTableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConversationTableViewController {
|
||||||
|
private enum LoadingState: Equatable {
|
||||||
|
case unloaded
|
||||||
|
case loadingMain
|
||||||
|
case loadedMain
|
||||||
|
case loadingContext
|
||||||
|
case loadedAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: TuskerNavigationDelegate {
|
extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||||
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
||||||
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||||
|
@ -419,3 +474,6 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching, Sta
|
||||||
cancelPrefetchingStatuses(with: ids)
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConversationTableViewController: ToastableViewController {
|
||||||
|
}
|
||||||
|
|
|
@ -121,11 +121,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
|
|
||||||
case let .failure(.client(error)):
|
case let .failure(.client(error)):
|
||||||
self.state = .unloaded
|
self.state = .unloaded
|
||||||
var config = ToastConfiguration(title: "Error Loading")
|
let config = ToastConfiguration(from: error, with: "Error Loading") { [weak self] (toast) in
|
||||||
config.subtitle = error.localizedDescription
|
|
||||||
config.systemImageName = error.systemImageName
|
|
||||||
config.actionTitle = "Retry"
|
|
||||||
config.action = { [weak self] (toast) in
|
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
self?.loadInitial()
|
self?.loadInitial()
|
||||||
}
|
}
|
||||||
|
@ -157,11 +153,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
case let .failure(.client(error)):
|
case let .failure(.client(error)):
|
||||||
var config = ToastConfiguration(title: "Error Loading Older")
|
let config = ToastConfiguration(from: error, with: "Error Loading Older") { [weak self] (toast) in
|
||||||
config.subtitle = error.localizedDescription
|
|
||||||
config.systemImageName = error.systemImageName
|
|
||||||
config.actionTitle = "Retry"
|
|
||||||
config.action = { [weak self] (toast) in
|
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
self?.loadOlder()
|
self?.loadOlder()
|
||||||
}
|
}
|
||||||
|
@ -244,11 +236,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .failure(.client(error)):
|
case let .failure(.client(error)):
|
||||||
var config = ToastConfiguration(title: "Error Loading Newer")
|
let config = ToastConfiguration(from: error, with: "Error Loading Newer") { [weak self] (toast) in
|
||||||
config.subtitle = error.localizedDescription
|
|
||||||
config.systemImageName = error.systemImageName
|
|
||||||
config.actionTitle = "Retry"
|
|
||||||
config.action = { [weak self] (toast) in
|
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
self?.refresh()
|
self?.refresh()
|
||||||
}
|
}
|
||||||
|
@ -326,14 +314,3 @@ extension DiffableTimelineLikeTableViewController: BackgroundableViewController
|
||||||
|
|
||||||
extension DiffableTimelineLikeTableViewController: ToastableViewController {
|
extension DiffableTimelineLikeTableViewController: ToastableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension Client.Error {
|
|
||||||
var systemImageName: String {
|
|
||||||
switch self {
|
|
||||||
case .networkError(_):
|
|
||||||
return "wifi.exclamationmark"
|
|
||||||
default:
|
|
||||||
return "exclamationmark.triangle"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
struct ToastConfiguration {
|
struct ToastConfiguration {
|
||||||
var systemImageName: String?
|
var systemImageName: String?
|
||||||
|
@ -31,3 +32,24 @@ struct ToastConfiguration {
|
||||||
case automatic
|
case automatic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ToastConfiguration {
|
||||||
|
init(from error: Client.Error, with title: String, retryAction: @escaping (ToastView) -> Void) {
|
||||||
|
self.init(title: title)
|
||||||
|
self.subtitle = error.localizedDescription
|
||||||
|
self.systemImageName = error.systemImageName
|
||||||
|
self.actionTitle = "Retry"
|
||||||
|
self.action = retryAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension Client.Error {
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .networkError(_):
|
||||||
|
return "wifi.exclamationmark"
|
||||||
|
default:
|
||||||
|
return "exclamationmark.triangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue