|
|
@ -6,8 +6,243 @@ |
|
|
|
// Copyright © 2018 Elpha. All rights reserved. |
|
|
|
// |
|
|
|
|
|
|
|
import AlamofireImage |
|
|
|
import CoreData |
|
|
|
import MastodonKit |
|
|
|
import UIKit |
|
|
|
|
|
|
|
enum StatusType { |
|
|
|
case ancestor |
|
|
|
case status |
|
|
|
case descendant |
|
|
|
} |
|
|
|
|
|
|
|
class StatusTableViewController: UITableViewController { |
|
|
|
public var status: StatusMO? = nil |
|
|
|
private var statuses: Dictionary<StatusType, [StatusMO]> = [:] |
|
|
|
|
|
|
|
var loading: Bool = false { |
|
|
|
didSet { |
|
|
|
DispatchQueue.main.async { |
|
|
|
if self.loading { |
|
|
|
self.refreshControl?.beginRefreshing() |
|
|
|
} else { |
|
|
|
self.refreshControl?.endRefreshing() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
override func viewDidLoad() { |
|
|
|
super.viewDidLoad() |
|
|
|
// navigationItem.title = "Toot" |
|
|
|
} |
|
|
|
|
|
|
|
override func viewDidAppear(_ animated: Bool) { |
|
|
|
super.viewDidAppear(animated) |
|
|
|
|
|
|
|
fetchStatuses { error in |
|
|
|
if error != nil { |
|
|
|
print("\(String(describing: error))") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func fetchStatuses(completion: @escaping (Error?) -> Void) { |
|
|
|
guard let client = AuthenticationManager.shared.mkClient else { |
|
|
|
completion(nil) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if let status = status { |
|
|
|
loading = true |
|
|
|
|
|
|
|
let statusRequest = Statuses.status(id: status.id!) |
|
|
|
let contextRequest = Statuses.context(id: status.id!) |
|
|
|
|
|
|
|
client.run(statusRequest) { result in |
|
|
|
switch result { |
|
|
|
case .success(let remoteStatus, _): |
|
|
|
DispatchQueue.main.async { |
|
|
|
_ = MastodonDataManager.upsertStatus(remoteStatus) |
|
|
|
} |
|
|
|
|
|
|
|
client.run(contextRequest) { result in |
|
|
|
switch result { |
|
|
|
case .success(let context, _): |
|
|
|
DispatchQueue.main.async { |
|
|
|
context.ancestors.forEach { remoteAncestor in |
|
|
|
let ancestor = MastodonDataManager.upsertStatus(remoteAncestor) |
|
|
|
status.addToAncestors(ancestor!.model) |
|
|
|
} |
|
|
|
|
|
|
|
context.ancestors.forEach { remoteDescendant in |
|
|
|
let descendant = MastodonDataManager.upsertStatus(remoteDescendant) |
|
|
|
status.addToDescendants(descendant!.model) |
|
|
|
} |
|
|
|
|
|
|
|
CoreDataManager.shared.saveContext() |
|
|
|
|
|
|
|
self.loading = false |
|
|
|
self.loadStatuses() |
|
|
|
self.tableView.reloadData() |
|
|
|
|
|
|
|
completion(nil) |
|
|
|
} |
|
|
|
case .failure(let error): |
|
|
|
completion(error) |
|
|
|
} |
|
|
|
} |
|
|
|
case .failure(let error): |
|
|
|
completion(error) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func loadStatuses() { |
|
|
|
print("Running loadStatuses()") |
|
|
|
|
|
|
|
guard let status = status else { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
let request = NSFetchRequest<StatusMO>(entityName: "Status") |
|
|
|
request.predicate = NSPredicate(format: "id == %@ OR ANY ancestors == %@ OR ANY descendants == %@", status.id!, status, status) |
|
|
|
request.sortDescriptors = [ |
|
|
|
NSSortDescriptor(key: "createdAt", ascending: false), |
|
|
|
] |
|
|
|
|
|
|
|
do { |
|
|
|
let statuses = try CoreDataManager.shared.context.fetch(request) |
|
|
|
self.statuses = Dictionary(grouping: statuses) { s -> StatusType in |
|
|
|
switch s.createdAt!.compare(status.createdAt!) { |
|
|
|
case .orderedSame: |
|
|
|
return .status |
|
|
|
case .orderedAscending: |
|
|
|
return .ancestor |
|
|
|
case .orderedDescending: |
|
|
|
return .descendant |
|
|
|
} |
|
|
|
} |
|
|
|
} catch { |
|
|
|
print("\(error)") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
extension StatusTableViewController { |
|
|
|
override func numberOfSections(in tableView: UITableView) -> Int { |
|
|
|
return statuses.count |
|
|
|
} |
|
|
|
|
|
|
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { |
|
|
|
if statuses.count == 1 { |
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
switch Array(statuses.keys)[section] { |
|
|
|
case .status: |
|
|
|
return "Toot" |
|
|
|
case .ancestor: |
|
|
|
return "In Reply To" |
|
|
|
case .descendant: |
|
|
|
return "Replies" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
|
|
|
return Array(statuses.values)[section].count |
|
|
|
} |
|
|
|
|
|
|
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
|
|
|
let statuses = Array(self.statuses.values)[indexPath.section] |
|
|
|
let status = statuses[indexPath.row] |
|
|
|
|
|
|
|
if status == self.status { |
|
|
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MainStatusTableViewCell", for: indexPath) as? MainStatusTableViewCell else { |
|
|
|
fatalError("Unable to find reusable cell") |
|
|
|
} |
|
|
|
|
|
|
|
let avatarFilter = AspectScaledToFillSizeWithRoundedCornersFilter( |
|
|
|
size: CGSize(width: 40.0, height: 40.0), |
|
|
|
radius: 30.0, |
|
|
|
divideRadiusByImageScale: true |
|
|
|
) |
|
|
|
|
|
|
|
func updateAccountView(status: StatusMO) { |
|
|
|
if let account = status.account { |
|
|
|
cell.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter) |
|
|
|
cell.displayNameLabel.text = account.displayName |
|
|
|
cell.usernameLabel.text = account.acct |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if let reblog = status.reblog { |
|
|
|
updateAccountView(status: reblog) |
|
|
|
} else { |
|
|
|
updateAccountView(status: status) |
|
|
|
} |
|
|
|
|
|
|
|
if let content = status.content { |
|
|
|
do { |
|
|
|
let styledContent = "<style>html * { font-size: 15px; color: #170c49; font-family: -apple-system } p { margin: 0; padding: 0 }</style> \(content)" |
|
|
|
let attributedText = try NSAttributedString( |
|
|
|
data: styledContent.data(using: String.Encoding.unicode, allowLossyConversion: true)!, |
|
|
|
options: [.documentType: NSAttributedString.DocumentType.html], |
|
|
|
documentAttributes: nil |
|
|
|
) |
|
|
|
|
|
|
|
cell.contentLabel.attributedText = attributedText |
|
|
|
} catch { |
|
|
|
print("\(error)") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return cell |
|
|
|
} else { |
|
|
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "StatusTableViewCell", for: indexPath) as? StatusTableViewCell else { |
|
|
|
fatalError("Unable to find reusable cell") |
|
|
|
} |
|
|
|
|
|
|
|
cell.statusView.delegate = self |
|
|
|
cell.statusView.update(withStatus: status) |
|
|
|
cell.statusView.replyView.isHidden = true |
|
|
|
|
|
|
|
if !loading { |
|
|
|
let statusAge = Calendar.current.dateComponents([.minute], from: status.fetchedAt!, to: Date()) |
|
|
|
let stale = statusAge.minute! > 30 |
|
|
|
|
|
|
|
if stale { |
|
|
|
fetchStatuses { error in |
|
|
|
if error != nil { |
|
|
|
print("\(String(describing: error))") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return cell |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
extension StatusTableViewController: StatusViewDelegate { |
|
|
|
func accountTapped(account: AccountMO) { |
|
|
|
let storyboard = UIStoryboard(name: "Main", bundle: nil) |
|
|
|
|
|
|
|
if let controller = storyboard.instantiateViewController(withIdentifier: "AccountTableViewController") as? AccountTableViewController { |
|
|
|
controller.account = account |
|
|
|
self.navigationController?.pushViewController(controller, animated: true) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func statusTapped(status: StatusMO) { |
|
|
|
let storyboard = UIStoryboard(name: "Main", bundle: nil) |
|
|
|
|
|
|
|
if let controller = storyboard.instantiateViewController(withIdentifier: "StatusTableViewController") as? StatusTableViewController { |
|
|
|
controller.status = status |
|
|
|
self.navigationController?.pushViewController(controller, animated: true) |
|
|
|
} |
|
|
|
} |
|
|
|
} |