Show better message when opening conv for deleted status
Also split conversation loading out into separate view controller
This commit is contained in:
parent
39bff06897
commit
6f006adbc1
|
@ -150,6 +150,8 @@
|
|||
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */; };
|
||||
D65B4B5A29720AB000DABDFB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B5929720AB000DABDFB /* ReportStatusView.swift */; };
|
||||
D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B5D2973040D00DABDFB /* ReportAddStatusView.swift */; };
|
||||
D65B4B6229771A3F00DABDFB /* FetchStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B6129771A3F00DABDFB /* FetchStatusService.swift */; };
|
||||
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */; };
|
||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||
|
@ -537,6 +539,8 @@
|
|||
D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSelectRulesView.swift; sourceTree = "<group>"; };
|
||||
D65B4B5929720AB000DABDFB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
||||
D65B4B5D2973040D00DABDFB /* ReportAddStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportAddStatusView.swift; sourceTree = "<group>"; };
|
||||
D65B4B6129771A3F00DABDFB /* FetchStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusService.swift; sourceTree = "<group>"; };
|
||||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; };
|
||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
|
||||
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -1033,6 +1037,7 @@
|
|||
D641C785213DD83B004B4513 /* Conversation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
||||
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */,
|
||||
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */,
|
||||
|
@ -1606,6 +1611,7 @@
|
|||
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */,
|
||||
D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */,
|
||||
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */,
|
||||
D65B4B6129771A3F00DABDFB /* FetchStatusService.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2008,6 +2014,7 @@
|
|||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
||||
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
|
||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */,
|
||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
||||
|
@ -2027,6 +2034,7 @@
|
|||
D659F36229541065002D944A /* TTTView.swift in Sources */,
|
||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */,
|
||||
D65B4B6229771A3F00DABDFB /* FetchStatusService.swift in Sources */,
|
||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */,
|
||||
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// FetchStatusService.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/17/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
class FetchStatusService {
|
||||
let statusID: String
|
||||
let mastodonController: MastodonController
|
||||
|
||||
init(statusID: String, mastodonController: MastodonController) {
|
||||
self.statusID = statusID
|
||||
self.mastodonController = mastodonController
|
||||
}
|
||||
|
||||
func run() async -> Result {
|
||||
let response = await mastodonController.runResponse(Client.getStatus(id: statusID))
|
||||
switch response {
|
||||
case .success(let status, _):
|
||||
return .loaded(status)
|
||||
case .failure(let error):
|
||||
switch error.type {
|
||||
case .unexpectedStatus(404), .mastodonError(404, _):
|
||||
self.handleStatusNotFound()
|
||||
return .notFound
|
||||
default:
|
||||
return .error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStatusNotFound() {
|
||||
// todo: remove from persistent store, send notifications
|
||||
}
|
||||
|
||||
enum Result {
|
||||
case loaded(Status)
|
||||
case notFound
|
||||
case error(Client.Error)
|
||||
}
|
||||
}
|
|
@ -97,19 +97,24 @@ class MastodonController: ObservableObject {
|
|||
return client.run(request, completion: completion)
|
||||
}
|
||||
|
||||
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||
let result: (Result, Pagination?) = try await withCheckedThrowingContinuation({ continuation in
|
||||
func runResponse<Result>(_ request: Request<Result>) async -> Response<Result> {
|
||||
let response = await withCheckedContinuation({ continuation in
|
||||
client.run(request) { response in
|
||||
switch response {
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
case .success(let result, let pagination):
|
||||
continuation.resume(returning: (result, pagination))
|
||||
}
|
||||
continuation.resume(returning: response)
|
||||
}
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||
let response = await runResponse(request)
|
||||
try Task.checkCancellation()
|
||||
return result
|
||||
switch response {
|
||||
case .failure(let error):
|
||||
throw error
|
||||
case .success(let result, let pagination):
|
||||
return (result, pagination)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Returns: A tuple of client ID and client secret.
|
||||
|
|
|
@ -75,7 +75,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
case .showConversation:
|
||||
guard let id = UserActivityManager.getConversationStatus(from: activity) else { return nil }
|
||||
return ConversationTableViewController(for: id, mastodonController: mastodonController)
|
||||
return ConversationViewController(for: id, state: .unknown, mastodonController: mastodonController)
|
||||
|
||||
case .checkNotifications:
|
||||
guard let mode = UserActivityManager.getNotificationsMode(from: activity) else { return nil }
|
||||
|
|
|
@ -22,11 +22,6 @@ class ConversationNode {
|
|||
|
||||
class ConversationTableViewController: EnhancedTableViewController {
|
||||
|
||||
static let showPostsImage = UIImage(systemName: "eye.fill")!
|
||||
static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
|
||||
|
||||
static let bottomSeparatorTag = 101
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
let mainStatusID: String
|
||||
|
@ -35,11 +30,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
var showStatusesAutomatically = false
|
||||
var visibilityBarButtonItem: UIBarButtonItem!
|
||||
|
||||
private var loadingState = LoadingState.unloaded
|
||||
|
||||
init(for mainStatusID: String, state: CollapseState = .unknown, mastodonController: MastodonController) {
|
||||
init(for mainStatusID: String, state: CollapseState, mastodonController: MastodonController) {
|
||||
self.mainStatusID = mainStatusID
|
||||
self.mainStatusState = state
|
||||
self.statusIDToScrollToOnLoad = mainStatusID
|
||||
|
@ -57,8 +49,6 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = NSLocalizedString("Conversation", comment: "conversation screen title")
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.prefetchDataSource = self
|
||||
|
@ -99,9 +89,9 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
|
||||
cell.setShowThreadLinks(prev: !firstInSection, next: !lastInSection)
|
||||
if lastInSection {
|
||||
if cell.viewWithTag(ConversationTableViewController.bottomSeparatorTag) == nil {
|
||||
if cell.viewWithTag(ViewTags.conversationBottomSeparator) == nil {
|
||||
let separator = UIView()
|
||||
separator.tag = ConversationTableViewController.bottomSeparatorTag
|
||||
separator.tag = ViewTags.conversationBottomSeparator
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.backgroundColor = tableView.separatorColor
|
||||
cell.addSubview(separator)
|
||||
|
@ -113,7 +103,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
])
|
||||
}
|
||||
} else {
|
||||
cell.viewWithTag(ConversationTableViewController.bottomSeparatorTag)?.removeFromSuperview()
|
||||
cell.viewWithTag(ViewTags.conversationBottomSeparator)?.removeFromSuperview()
|
||||
}
|
||||
|
||||
return cell
|
||||
|
@ -124,97 +114,24 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
return cell
|
||||
}
|
||||
})
|
||||
|
||||
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||
updateVisibilityBarButtonItem()
|
||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
|
||||
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithDefaultBackground()
|
||||
navigationItem.scrollEdgeAppearance = appearance
|
||||
|
||||
Task {
|
||||
await loadMainStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibilityBarButtonItem() {
|
||||
visibilityBarButtonItem.isSelected = showStatusesAutomatically
|
||||
visibilityBarButtonItem.accessibilityLabel = showStatusesAutomatically ? "Collapse All" : "Expand All"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadMainStatus() async {
|
||||
guard loadingState == .unloaded else { return }
|
||||
func addMainStatus(_ status: StatusMO) {
|
||||
loadViewIfNeeded()
|
||||
|
||||
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||
await mainStatusLoaded(mainStatus)
|
||||
} else {
|
||||
loadingState = .loadingMain
|
||||
let req = Client.getStatus(id: mainStatusID)
|
||||
do {
|
||||
let (status, _) = try await mastodonController.run(req)
|
||||
let statusMO = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||
await mainStatusLoaded(statusMO)
|
||||
} catch {
|
||||
let error = error as! Client.Error
|
||||
loadingState = .unloaded
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self?.loadMainStatus()
|
||||
}
|
||||
showToast(configuration: config, animated: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
loadingState = .loadedMain
|
||||
|
||||
await loadContext(for: mainStatus)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadContext(for mainStatus: StatusMO) async {
|
||||
guard loadingState == .loadedMain else { return }
|
||||
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
||||
let parentIDs = self.getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||
|
||||
loadingState = .loadingContext
|
||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||
|
||||
// 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
|
||||
let request = Status.getContext(mainStatusID)
|
||||
do {
|
||||
let (context, _) = try await mastodonController.run(request)
|
||||
let parentIDs = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||
|
||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||
self.contextLoaded(mainStatus: mainStatus, context: context, parentIDs: parentIDs)
|
||||
|
||||
} catch {
|
||||
let error = error as! Client.Error
|
||||
self.loadingState = .loadedMain
|
||||
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in
|
||||
toast.dismissToast(animated: true)
|
||||
await self?.loadContext(for: mainStatus)
|
||||
}
|
||||
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()
|
||||
|
@ -249,8 +166,6 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
self.tableView.scrollToRow(at: indexPath, at: position, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.loadingState = .loadedAll
|
||||
}
|
||||
|
||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
|
@ -377,10 +292,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||
}
|
||||
|
||||
@objc func toggleVisibilityButtonPressed() {
|
||||
showStatusesAutomatically = !showStatusesAutomatically
|
||||
|
||||
|
||||
func updateVisibleCellCollapseState() {
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||
state.collapsed = !showStatusesAutomatically
|
||||
|
@ -396,10 +309,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
// recalculate cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
|
||||
updateVisibilityBarButtonItem()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ConversationTableViewController {
|
||||
|
@ -436,21 +346,11 @@ extension ConversationTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController {
|
||||
private enum LoadingState: Equatable {
|
||||
case unloaded
|
||||
case loadingMain
|
||||
case loadedMain
|
||||
case loadingContext
|
||||
case loadedAll
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationTableViewController {
|
||||
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController {
|
||||
let vc = ConversationViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||
// transfer show statuses automatically state when showing new conversation
|
||||
vc.showStatusesAutomatically = self.showStatusesAutomatically
|
||||
return vc
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
//
|
||||
// ConversationViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/17/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class ConversationViewController: UIViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
let mainStatusID: String
|
||||
let mainStatusState: CollapseState
|
||||
var statusIDToScrollToOnLoad: String {
|
||||
didSet {
|
||||
if case .displaying(let vc) = state {
|
||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
|
||||
}
|
||||
}
|
||||
}
|
||||
var showStatusesAutomatically = false {
|
||||
didSet {
|
||||
if case .displaying(let vc) = state {
|
||||
vc.showStatusesAutomatically = showStatusesAutomatically
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var collapseBarButtonItem: UIBarButtonItem!
|
||||
|
||||
private var state: State = .unloaded {
|
||||
didSet {
|
||||
switch oldValue {
|
||||
case .loading(let indicator):
|
||||
indicator.removeFromSuperview()
|
||||
case .displaying(let vc):
|
||||
vc.removeViewAndController()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .unloaded:
|
||||
break
|
||||
case .loading(let indicator):
|
||||
indicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(indicator)
|
||||
NSLayoutConstraint.activate([
|
||||
indicator.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
|
||||
indicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||
])
|
||||
case .displaying(let vc):
|
||||
embedChild(vc)
|
||||
case .notFound:
|
||||
showMainStatusNotFound()
|
||||
}
|
||||
|
||||
updateVisibilityBarButtonItem()
|
||||
}
|
||||
}
|
||||
|
||||
init(for mainStatusID: String, state mainStatusState: CollapseState, mastodonController: MastodonController) {
|
||||
self.mainStatusID = mainStatusID
|
||||
self.mainStatusState = mainStatusState
|
||||
self.statusIDToScrollToOnLoad = mainStatusID
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = NSLocalizedString("Conversation", comment: "conversation screen title")
|
||||
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
|
||||
collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
|
||||
updateVisibilityBarButtonItem()
|
||||
navigationItem.rightBarButtonItem = collapseBarButtonItem
|
||||
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
|
||||
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithDefaultBackground()
|
||||
navigationItem.scrollEdgeAppearance = appearance
|
||||
}
|
||||
|
||||
private func updateVisibilityBarButtonItem() {
|
||||
switch state {
|
||||
case .loading(_), .displaying(_):
|
||||
collapseBarButtonItem.isEnabled = true
|
||||
default:
|
||||
collapseBarButtonItem.isEnabled = false
|
||||
}
|
||||
|
||||
collapseBarButtonItem.isSelected = showStatusesAutomatically
|
||||
collapseBarButtonItem.accessibilityLabel = showStatusesAutomatically ? "Collapse All" : "Expand All"
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
Task {
|
||||
await loadMainStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Loading
|
||||
|
||||
private func loadMainStatus() async {
|
||||
@MainActor
|
||||
func doLoadMainStatus() async -> StatusMO? {
|
||||
switch await FetchStatusService(statusID: mainStatusID, mastodonController: mastodonController).run() {
|
||||
case .loaded(let status):
|
||||
return mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||
case .notFound:
|
||||
state = .notFound
|
||||
showMainStatusNotFound()
|
||||
return nil
|
||||
case .error(let error):
|
||||
self.showMainStatusError(error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let cached = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||
// if we have a cached copy, display it immediately but still try to refresh it
|
||||
Task {
|
||||
await doLoadMainStatus()
|
||||
}
|
||||
await mainStatusLoaded(cached)
|
||||
} else {
|
||||
// otherwise, show a loading indicator while loading the main status
|
||||
let indicator = UIActivityIndicatorView(style: .medium)
|
||||
indicator.startAnimating()
|
||||
state = .loading(indicator)
|
||||
|
||||
if let status = await doLoadMainStatus() {
|
||||
await mainStatusLoaded(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
||||
let vc = ConversationTableViewController(for: mainStatusID, state: mainStatusState, mastodonController: mastodonController)
|
||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
|
||||
vc.showStatusesAutomatically = showStatusesAutomatically
|
||||
vc.addMainStatus(mainStatus)
|
||||
state = .displaying(vc)
|
||||
|
||||
await loadContext(for: mainStatus)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadContext(for mainStatus: StatusMO) async {
|
||||
guard case .displaying(_) = state else {
|
||||
return
|
||||
}
|
||||
|
||||
let request = Status.getContext(mainStatus.id)
|
||||
do {
|
||||
let (context, _) = try await mastodonController.run(request)
|
||||
guard case .displaying(let vc) = state else {
|
||||
return
|
||||
}
|
||||
|
||||
await vc.addContext(context, for: mainStatus)
|
||||
} catch {
|
||||
guard case .displaying(_) = state else {
|
||||
return
|
||||
}
|
||||
let error = error as! Client.Error
|
||||
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self?.loadContext(for: mainStatus)
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func showMainStatusNotFound() {
|
||||
let emoji = UILabel()
|
||||
emoji.font = .systemFont(ofSize: 64)
|
||||
emoji.text = "🤷"
|
||||
|
||||
let title = UILabel()
|
||||
title.textColor = .secondaryLabel
|
||||
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||
title.adjustsFontForContentSizeCategory = true
|
||||
title.text = "Not Found"
|
||||
|
||||
let subtitle = UILabel()
|
||||
subtitle.textColor = .secondaryLabel
|
||||
subtitle.font = .preferredFont(forTextStyle: .body)
|
||||
subtitle.adjustsFontForContentSizeCategory = true
|
||||
subtitle.text = "The post you are looking for may have been deleted, or may not be visible to you."
|
||||
subtitle.numberOfLines = 0
|
||||
subtitle.textAlignment = .center
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
emoji,
|
||||
title,
|
||||
subtitle,
|
||||
])
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .center
|
||||
stack.spacing = 8
|
||||
stack.isAccessibilityElement = true
|
||||
stack.accessibilityLabel = "\(title.text!). \(subtitle.text!)"
|
||||
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
|
||||
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
||||
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
private func showMainStatusError(_ error: Client.Error) {
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self?.loadMainStatus()
|
||||
}
|
||||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc func toggleCollapseButtonPressed() {
|
||||
guard case .displaying(let vc) = state else {
|
||||
return
|
||||
}
|
||||
showStatusesAutomatically = !showStatusesAutomatically
|
||||
vc.updateVisibleCellCollapseState()
|
||||
updateVisibilityBarButtonItem()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ConversationViewController {
|
||||
enum State {
|
||||
case unloaded
|
||||
case loading(UIActivityIndicatorView)
|
||||
case displaying(ConversationTableViewController)
|
||||
case notFound
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationViewController: ToastableViewController {
|
||||
}
|
|
@ -13,7 +13,7 @@ import Pachyderm
|
|||
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
||||
var apiController: MastodonController! { get }
|
||||
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationTableViewController
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController
|
||||
}
|
||||
|
||||
extension TuskerNavigationDelegate {
|
||||
|
@ -78,8 +78,8 @@ extension TuskerNavigationDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationTableViewController {
|
||||
return ConversationTableViewController(for: mainStatusID, state: state, mastodonController: apiController)
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController {
|
||||
return ConversationViewController(for: mainStatusID, state: state, mastodonController: apiController)
|
||||
}
|
||||
|
||||
func selected(status statusID: String) {
|
||||
|
|
|
@ -16,4 +16,5 @@ struct ViewTags {
|
|||
static let navEmptyTitleView = 42003
|
||||
static let splitNavCloseSecondaryButton = 42004
|
||||
static let customAlertSeparator = 42005
|
||||
static let conversationBottomSeparator = 42006
|
||||
}
|
||||
|
|
|
@ -759,7 +759,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
return nil
|
||||
}
|
||||
return UIContextMenuConfiguration {
|
||||
ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
|
||||
ConversationViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
UIMenu(children: self.delegate!.actionsForStatus(status, source: .view(self)))
|
||||
}
|
||||
|
|
|
@ -433,7 +433,7 @@ extension TimelineStatusTableViewCell: MenuPreviewProvider {
|
|||
return nil
|
||||
}
|
||||
return (
|
||||
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
||||
content: { ConversationViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
||||
actions: { self.delegate?.actionsForStatus(status, source: .view(self)) ?? [] }
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue