Fix crash when conversation loading fails

This commit is contained in:
Shadowfacts 2021-11-10 17:15:12 -05:00
parent 30ef9cc6d0
commit b917120f17
3 changed files with 115 additions and 58 deletions

View File

@ -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 {
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}