// // AccountTableView.swift // elpha-ios // // Created by Dwayne Harris on 10/9/18. // Copyright © 2018 Elpha. All rights reserved. // import CoreData import Kingfisher import UIKit import SafariServices class FieldTableViewController: NSObject, UITableViewDelegate, UITableViewDataSource { var fields: [AccountField]? func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fields?.count ?? 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "FieldTableViewCell", for: indexPath) as? FieldTableViewCell else { fatalError("Unable to find reusable cell") } if let fields = fields { let field = fields[indexPath.row] cell.nameLabel.text = field.name do { let htmlString = """ \(field.value) """ if let data = htmlString.data(using: String.Encoding.utf16) { cell.valueTextView.attributedText = try NSAttributedString( data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil ) } } catch { print("\(error)") } } return cell } } class AccountTableViewController: AbstractStatusTableViewController { @IBOutlet var headerView: UIView! @IBOutlet var headerImageView: UIImageView! @IBOutlet var avatarImageView: UIImageView! @IBOutlet var displayNameLabel: UILabel! @IBOutlet var usernameLabel: UILabel! @IBOutlet var statusesLabel: UILabel! @IBOutlet var followingLabel: UILabel! @IBOutlet var followersLabel: UILabel! @IBOutlet var statusTypeSegmentedControl: UISegmentedControl! @IBOutlet var contentTextView: UITextViewFixed! @IBOutlet var fieldTableView: UITableView! var account: AccountMO? var fieldTableViewController: FieldTableViewController? override var currentPaginationContext: String { get { guard let account = self.account else { return "" } return "account:\(account.acct!):\(statusTypeSegmentedControl.selectedSegmentIndex)" } } @IBAction func statusTypeChanged(_ sender: UISegmentedControl) { initializeFetchedResultsController() tableView.reloadData() fetch() } override func viewDidLoad() { super.viewDidLoad() refreshControl?.addTarget(self, action: #selector(self.fetch), for: .valueChanged) fieldTableViewController = FieldTableViewController() avatarImageView.setRoundedCorners() avatarImageView.setShadow() if self.account == nil { if let session = AuthenticationManager.session { self.account = session.account } } if let account = account { updateHeader(withAccount: account) // sizeHeaderToFit() initializeFetchedResultsController() } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let account = self.account { MastodonAPI.account(id: account.id!) { data, error in guard let data = data, error == nil else { return } self.account = MastodonDataManager.upsertAccount(data) if let account = self.account { DispatchQueue.main.async { self.updateHeader(withAccount: account) self.fetch() } } } } } private func sizeHeaderToFit() { headerView.translatesAutoresizingMaskIntoConstraints = false let headerWidth = headerView.bounds.size.width let headerWidthConstraints = NSLayoutConstraint.constraints(withVisualFormat: "[headerView(width)]", options: NSLayoutConstraint.FormatOptions(rawValue: UInt(0)), metrics: ["width": headerWidth], views: ["headerView": headerView]) headerView.addConstraints(headerWidthConstraints) headerView.setNeedsLayout() headerView.layoutIfNeeded() let headerSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let height = headerSize.height var frame = headerView.frame frame.size.height = height headerView.frame = frame headerView.removeConstraints(headerWidthConstraints) headerView.translatesAutoresizingMaskIntoConstraints = true } private func updateHeader(withAccount account: AccountMO) { navigationItem.title = account.displayName if let headerURL = account.headerURL { headerImageView.kf.setImage(with: headerURL) } if let avatarURL = account.avatarURL { let options: KingfisherOptionsInfo = SettingsManager.automaticallyPlayGIFs ? [] : [.onlyLoadFirstFrame] avatarImageView.kf.setImage(with: avatarURL, options: options) } displayNameLabel.text = account.displayName usernameLabel.text = "@\(account.acct!)" if let note = account.note { contentTextView.attributedText = note.htmlAttributed(size: 15, centered: true) } statusesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: account.statusesCount), number: .decimal) followingLabel.text = NumberFormatter.localizedString(from: NSNumber(value: account.followingCount), number: .decimal) followersLabel.text = NumberFormatter.localizedString(from: NSNumber(value: account.followersCount), number: .decimal) if let fields = account.fields, fields.count > 0 { fieldTableViewController!.fields = fields fieldTableView.dataSource = fieldTableViewController fieldTableView.delegate = fieldTableViewController fieldTableView.isHidden = false } else { fieldTableView.isHidden = true } } override func loadMoreTapped(_ sender: Any, status: StatusMO, direction: PaginationDirection) { func removeMarker(at: Int) { status.markers?.remove(at: at) CoreDataManager.shared.saveContext() } if let markers = status.markers { for (index, marker) in markers.enumerated() { if marker.context == self.currentPaginationContext && marker.item.direction == direction { fetchStatuses(withPagination: marker.item) { error in if error == nil { removeMarker(at: index) } else { AlertManager.shared.show(message: error!.localizedDescription, category: .error) } } } } } } @objc func fetch() { fetchStatuses { error in if error != nil { AlertManager.shared.show(message: error!.localizedDescription, category: .error) } } } func fetchStatuses(withPagination pagination: PaginationItem? = nil, completion: @escaping (Error?) -> Void) { if let account = account { func requestCompletion(data: JSONObjectArray?, pagination: [PaginationItem]?, error: Error?) { guard let data = data, error == nil else { self.loading = false completion(error) return } for (index, status) in data.enumerated() { if let upsertResult = MastodonDataManager.upsertStatus(status) { let status = upsertResult.object if let pagination = pagination { var markers: [PaginationMarker] = status.markers ?? [] if index == 0 { pagination.forEach { item in if item.direction == .prev { markers.append(PaginationMarker(context: self.currentPaginationContext, item: item)) } } } if index == data.count - 1 { pagination.forEach { item in if item.direction == .next { markers.append(PaginationMarker(context: self.currentPaginationContext, item: item)) } } } status.markers = markers } } } MastodonAPI.pinnedStatuses(accountID: account.id!, limit: fetchLimit) { data, error in guard let data = data, error == nil else { self.loading = false completion(error) return } data.forEach { status in _ = MastodonDataManager.upsertStatus(status.merging(["pinned": true]) { (_, new) in new }) } } CoreDataManager.shared.saveContext() self.loading = false completion(nil) } switch statusTypeSegmentedControl.selectedSegmentIndex { case 1: MastodonAPI.statuses( accountID: account.id!, onlyMedia: false, excludeReplies: false, limit: fetchLimit, pagination: pagination, completion: requestCompletion ) case 2: MastodonAPI.statuses( accountID: account.id!, onlyMedia: true, excludeReplies: true, limit: fetchLimit, pagination: pagination, completion: requestCompletion ) default: MastodonAPI.statuses( accountID: account.id!, onlyMedia: false, excludeReplies: true, limit: fetchLimit, pagination: pagination, completion: requestCompletion ) } } } } extension AccountTableViewController: NSFetchedResultsControllerDelegate { func initializeFetchedResultsController() { if let account = self.account { let request = NSFetchRequest(entityName: "Status") request.sortDescriptors = [ NSSortDescriptor(key: "pinned", ascending: false), NSSortDescriptor(key: "createdAt", ascending: false), ] switch statusTypeSegmentedControl.selectedSegmentIndex { case 1: request.predicate = NSPredicate(format: "account == %@", account) case 2: request.predicate = NSPredicate(format: "account == %@ AND attachments.@count > 0", account) default: request.predicate = NSPredicate(format: "account == %@ AND inReplyToID == nil", account) } fetchedResultsController = NSFetchedResultsController( fetchRequest: request, managedObjectContext: CoreDataManager.shared.context, sectionNameKeyPath: nil, cacheName: nil ) fetchedResultsController!.delegate = self do { try fetchedResultsController!.performFetch() } catch { print("\(error)") } } } func controllerWillChangeContent(_ controller: NSFetchedResultsController) { tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case NSFetchedResultsChangeType.insert: tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.none) case NSFetchedResultsChangeType.delete: tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.none) case NSFetchedResultsChangeType.update: if let cell = tableView.cellForRow(at: indexPath!) as? AccountTableViewCell { updateCell(cell, withStatusAt: indexPath!) } case NSFetchedResultsChangeType.move: tableView.deleteRows(at: [indexPath!], with: UITableView.RowAnimation.fade) tableView.insertRows(at: [newIndexPath!], with: UITableView.RowAnimation.fade) } } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { tableView.endUpdates() } override func numberOfSections(in tableView: UITableView) -> Int { return 1 } // override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // if let cell = cell as? AccountTableViewCell { // CellHeightManager.set(status: cell.statusView.status, height: cell.frame.size.height) // } // } // override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { // return CellHeightManager.get(status: fetchedResultsController?.object(at: indexPath)) // } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fetchedResultsController?.fetchedObjects?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "AccountTableViewCell", for: indexPath) as? AccountTableViewCell else { fatalError("Unable to find reusable cell") } updateCell(cell, withStatusAt: indexPath) cell.statusView.delegate = self return cell } }